diff --git a/doc/hacking.texi b/doc/hacking.texi index 226899d..f848bb6 100644 --- a/doc/hacking.texi +++ b/doc/hacking.texi @@ -341,14 +341,17 @@ If this is a concern, in conjunction with ease.js' @url{https://www.gnu.org/software/easejs/manual/easejs.html#Type-Checks-and-Polymorphism,@samp{Class.isA}}. -Interfaces do not exist at runtime in TypeScript, - but they do in easejs. -Consequently, - you can continue to export an ease.js interface while also exporting - a TypeScript interface. +Often times you will need to reference a class or interface as a + dependency before it has been migrated away from ease.js. To do this, - continue to export using @samp{module.exports} rather than - TypeScript's @samp{export =}. + create a corresponding @code{.d.ts} file in the same directory + as the dependency. +For example, + if a class @code{Foo} is contained in @file{Foo.js}, + create a sibling @file{Foo.d.ts} file. +For more information, + see @url{https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html,Declaration Files} + in the TypeScript handbook. ease.js implements stackable Scala-like traits. Traits are @emph{not} provided by TypeScript. @@ -446,7 +449,7 @@ This can be done using @verbatim type PositiveInteger = NominalType; -const isPositiveInteger = ( x: number ): n is PositiveInteger => n > 0; +const isPositiveInteger = ( n: number ): n is PositiveInteger => n > 0; const lookupIndex( arr: T[], i: PositiveInteger ): T => arr[ i ]; diff --git a/src/client/action/ClientAction.ts b/src/client/action/ClientAction.ts new file mode 100644 index 0000000..da57ebb --- /dev/null +++ b/src/client/action/ClientAction.ts @@ -0,0 +1,39 @@ +/** + * Representation of actions to be performed by the client + * + * 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 . + */ + + +/** + * Action to be performed by the client + * + * TODO: More specific types + */ +export interface ClientAction +{ + /** Action to be performed */ + action: string; + + /** Action arguments */ + [P: string]: any; +} + + +/** Set of actions */ +export type ClientActions = ClientAction[]; diff --git a/src/numeric.ts b/src/numeric.ts new file mode 100644 index 0000000..a86edef --- /dev/null +++ b/src/numeric.ts @@ -0,0 +1,43 @@ +/** + * Numeric types + * + * 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 . + * + * TypeScript's type system does not support algebraic numeric domains. A + * compromise is to provide nominal types that allow developers to assume + * that some constraint has been met, and then ensure that the type is only + * ever asserted when that constraint is explicitly validated at + * runtime. This allows us to have compile-time checks on numeric values + * under the assumption that the runtime will enforce them. + * + * For this to work, _it is important to always use type predicates_; + * if you explicit cast to one of these numeric types, it circumvents the + * safety provided by the system and may introduce nasty bugs, since users + * of these types assume the provided data has already been validated. + */ + +/** + * Any number ≥ 0 + * + * This is useful for array indexing. + */ +export type PositiveInteger = NominalType; + + +/** Whether the given number is suitable as a PositiveInteger */ +export const isPositiveInteger = ( n: number ): n is PositiveInteger => n >= 0; diff --git a/src/program/Program.d.ts b/src/program/Program.d.ts new file mode 100644 index 0000000..de3f45a --- /dev/null +++ b/src/program/Program.d.ts @@ -0,0 +1,27 @@ +/** + * Contains Program base class + * + * 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 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 General Public License + * along with this program. If not, see . + */ + +export declare abstract class Program +{ + readonly ineligibleLockCount: number; + + getId(): string; +} diff --git a/src/quote/BaseQuote.d.ts b/src/quote/BaseQuote.d.ts new file mode 100644 index 0000000..9af47ee --- /dev/null +++ b/src/quote/BaseQuote.d.ts @@ -0,0 +1,93 @@ +/** + * Contains program Quote class + * + * 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 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 General Public License + * along with this program. If not, see . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../program/Program"; +import { Quote, QuoteId } from "./Quote"; + + +export declare class BaseQuote implements Quote +{ + /** + * Retrieve Program associated with quote + * + * @return quote program + */ + getProgram(): Program; + + + /** + * Returns the program id associated with the quote + * + * @return program id + */ + getProgramId(): string; + + + /** + * Returns the quote id + * + * The quote id is immutable. A different quote id would represent a + * different quote, therefore a new object should be created with the + * desired quote id. + * + * @return quote id + */ + getId(): QuoteId; + + + /** + * Returns the id of the current step + * + * @return id of current step + */ + getCurrentStepId(): number; + + + /** + * Sets an explicit lock, providing a reason for doing so + * + * @param reason - lock reason + * @param step - step that user may not navigate prior + * + * @return self + */ + setExplicitLock( reason: string, step: number ): this; + + + /** + * Set the date that the premium was calculated as a Unix timestamp + * + * @param timestamp - Unix timestamp representing premium date + * + * @return self + */ + setLastPremiumDate( timestamp: UnixTimestamp ): this; + + + /** + * Retrieve the last time the premium was calculated + * + * @return last calculated time or 0 + */ + getLastPremiumDate(): UnixTimestamp; +} diff --git a/src/quote/Quote.d.ts b/src/quote/Quote.d.ts new file mode 100644 index 0000000..9324d0f --- /dev/null +++ b/src/quote/Quote.d.ts @@ -0,0 +1,33 @@ +/** + * Generic interface to represent quotes + * + * 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 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 General Public License + * along with this program. If not, see . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../program/Program"; +import { QuoteId } from "../document/Document"; + + +export declare interface Quote +{ + // TODO: the easejs interface is empty; is this actually needed? +} + +export { QuoteId }; diff --git a/src/server/Server.d.ts b/src/server/Server.d.ts new file mode 100644 index 0000000..e165d69 --- /dev/null +++ b/src/server/Server.d.ts @@ -0,0 +1,66 @@ +/** + * General server actions + * + * 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 . + */ + +import { ClientActions } from "../client/action/ClientAction"; +import { ServerSideQuote } from "./quote/ServerSideQuote"; +import { UserRequest } from "./request/UserRequest"; + + +/** + * General server actions + */ +export declare class Server +{ + /** + * Send response to user + * + * @param request - request to respond to + * @param quote - quote associated with request + * @param data - data with which to reply + * @param actions - optional client actions + * + * @return self + */ + sendResponse( + request: UserRequest, + quote: ServerSideQuote, + data: Record, + actions?: ClientActions, + ): this; + + + /** + * Send response to user + * + * @param request - request to respond to + * @param message - message to display to user + * @param actions - optional client actions + * @param btn_caption - optional caption for acknowledgement button + * + * @return self + */ + sendError( + request: UserRequest, + message: string, + actions?: ClientActions, + btn_caption?: string, + ): this; +} diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 7e8ca54..0f9cfa3 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -28,6 +28,8 @@ const { ReplSetServers: ReplSetServers, } = require( 'mongodb/lib/mongodb' ); +const easejs = require( 'easejs' ); + const regex_base = /^\/quote\/([a-z0-9-]+)\/?(?:\/(\d+)\/?(?:\/(.*))?|\/(program.js))?$/; const regex_step = /^step\/(\d+)\/?(?:\/(post|visit))?$/; @@ -83,9 +85,8 @@ const { ExportService, }, - RatingService, + RatingService: { RatingService }, RatingServicePublish, - RatingServiceSubmitNotify, TokenedService, }, @@ -137,7 +138,7 @@ exports.init = function( logger, enc_service, conf ) server.init( server_cache, exports.rater ); // TODO: temporary proof-of-concept - rating_service = RatingService.use( + rating_service = easejs( RatingService ).use( RatingServicePublish( amqplib, exports.post_rate_publish, logger ) )( logger, dao, server, exports.rater @@ -535,11 +536,9 @@ function doRoute( program, request, data, resolve, reject ) { var response = UserResponse( request ); - rating_service.request( request, response, quote, alias, function() - { - // we're done; free the lock - free(); - } ); + rating_service.request( request, response, quote, alias ) + .catch( () => {} ) + .then( () => free() ); } ); }, true ); } diff --git a/src/server/db/MongoServerDao.d.ts b/src/server/db/MongoServerDao.d.ts new file mode 100644 index 0000000..18492cc --- /dev/null +++ b/src/server/db/MongoServerDao.d.ts @@ -0,0 +1,145 @@ +/** + * Mongo DB DAO for program server + * + * 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 . + */ + +import { ServerDao, Callback } from "./ServerDao"; + +import { ClassificationData, WorksheetData } from "../rater/Rater"; +import { PositiveInteger } from "../../numeric"; +import { QuoteId } from "../../document/Document"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; + + +/** + * MongoDB-backed data store + */ +export declare class MongoServerDao implements ServerDao +{ + /** + * Saves a quote to the database + * + * A full save will include all metadata. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + * @param save_data - quote data to save (optional) + */ + saveQuote( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + save_data: Record, + ): this; + + + /** + * Merges bucket data with the existing bucket (rather than overwriting the + * entire bucket) + * + * @param quote - quote to save + * @param data - bucket data + * @param success - successful callback + * @param failure - failure callback + */ + mergeBucket( + quote: ServerSideQuote, + data: Record, + success: Callback, + failure: Callback, + ): this; + + + /** + * Save quote classification data + * + * @param quote - quote to save + * @param classes - classification data + * @param success - successful callback + * @param failure - failure callback + */ + saveQuoteClasses( + quote: ServerSideQuote, + classes: ClassificationData, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote state to the database + * + * The quote state includes the current step, the top visited step and the + * explicit lock message. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote lock state to the database + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteLockState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this + + + /** + * Save worksheet data + * + * @param qid - quote identifier + * @param data - worksheet data + * @param callback - callback + */ + setWorksheets( + qid: QuoteId, + data: WorksheetData, + callback: NodeCallback, + ): this; + + + /** + * Retrieve worksheet data + * + * @param qid - quote identifier + * @param supplier - supplier id + * @param index - worksheet index + * @param callback - callback + */ + getWorksheet( + qid: QuoteId, + supplier: string, + index: PositiveInteger, + callback: ( data: WorksheetData | null ) => void, + ): this; +} diff --git a/src/server/db/ServerDao.d.ts b/src/server/db/ServerDao.d.ts new file mode 100644 index 0000000..9c8ce32 --- /dev/null +++ b/src/server/db/ServerDao.d.ts @@ -0,0 +1,149 @@ +/** + * General server database operations + * + * 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 . + * + * This interface was created to satisfy MongoServerDao and may not be + * sufficiently general for other database abstractions. + */ + +import { ClassificationData, WorksheetData } from "../rater/Rater"; +import { PositiveInteger } from "../../numeric"; +import { QuoteId } from "../../document/Document"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; + +/** Success or failure callback */ +export type Callback = ( quote: ServerSideQuote ) => void; + + +/** + * Database abstraction + */ +export interface ServerDao +{ + /** + * Saves a quote to the database + * + * A full save will include all metadata. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + * @param save_data - quote data to save (optional) + */ + saveQuote( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + save_data: Record, + ): this; + + + /** + * Merges bucket data with the existing bucket (rather than overwriting the + * entire bucket) + * + * @param quote - quote to save + * @param data - bucket data + * @param success - successful callback + * @param failure - failure callback + */ + mergeBucket( + quote: ServerSideQuote, + data: Record, + success: Callback, + failure: Callback, + ): this; + + + /** + * Save quote classification data + * + * @param quote - quote to save + * @param classes - classification data + * @param success - successful callback + * @param failure - failure callback + */ + saveQuoteClasses( + quote: ServerSideQuote, + classes: ClassificationData, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote state to the database + * + * The quote state includes the current step, the top visited step and the + * explicit lock message. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote lock state to the database + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteLockState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this + + + /** + * Save worksheet data + * + * @param qid - quote identifier + * @param data - worksheet data + * @param callback - callback + */ + setWorksheets( + qid: QuoteId, + data: WorksheetData, + callback: NodeCallback, + ): this; + + + /** + * Retrieve worksheet data + * + * @param qid - quote identifier + * @param supplier - supplier id + * @param index - worksheet index + * @param callback - callback + */ + getWorksheet( + qid: QuoteId, + supplier: string, + index: PositiveInteger, + callback: ( data: WorksheetData | null ) => void, + ): this; +} diff --git a/src/server/log/PriorityLog.d.ts b/src/server/log/PriorityLog.d.ts new file mode 100644 index 0000000..60819a4 --- /dev/null +++ b/src/server/log/PriorityLog.d.ts @@ -0,0 +1,45 @@ +/** + * Priority log typescript type definitions + * + * 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 . + */ + +export declare interface PriorityLog +{ + readonly PRIORITY_ERROR: number; + readonly PRIORITY_IMPORTANT: number; + readonly PRIORITY_DB: number; + readonly PRIORITY_INFO: number; + readonly PRIORITY_SOCKET: number; + + /** + * Write to the log at the given priority + * + * If the priority is less than or equal to the set priority for this + * object, it will be logged. Otherwise, the message will be ignored. + * + * The first argument should be the priority. The remaining arguments should + * be provided in a sprintf()-style fashion + * + * @param priority - logging priority + * @param ...args - sprintf-style aruments + * + * @return self + */ + log( priority: number, ...args: Array ): this; +} diff --git a/src/server/quote/ServerSideQuote.d.ts b/src/server/quote/ServerSideQuote.d.ts new file mode 100644 index 0000000..98eff98 --- /dev/null +++ b/src/server/quote/ServerSideQuote.d.ts @@ -0,0 +1,46 @@ +/** + * Augments a quote with additional data for use by the quote server + * + * 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 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 General Public License + * along with this program. If not, see . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../../program/Program"; +import { BaseQuote } from "../../quote/BaseQuote"; + + +export declare class ServerSideQuote extends BaseQuote +{ + /** + * Last rated date, if any + * + * @return last rated date + */ + getRatedDate(): UnixTimestamp; + + + /** + * Set the timestamp of the first time quote was rated + * + * @param timestamp - Unix timestamp representing first rated date + * + * @return self + */ + setRatedDate( timestamp: UnixTimestamp ): this; +} diff --git a/src/server/rater/DslRater.js b/src/server/rater/DslRater.js index 0437dbb..75bc1f1 100644 --- a/src/server/rater/DslRater.js +++ b/src/server/rater/DslRater.js @@ -20,7 +20,6 @@ */ var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), EventEmitter = require( 'events' ).EventEmitter, DslRaterContext = require( './DslRaterContext' ); diff --git a/src/server/rater/DslRaterContext.js b/src/server/rater/DslRaterContext.js index a9a837b..622eab1 100644 --- a/src/server/rater/DslRaterContext.js +++ b/src/server/rater/DslRaterContext.js @@ -20,7 +20,6 @@ */ var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), EventEmitter = require( 'events' ).EventEmitter, Quote = require( '../../quote/Quote' ); diff --git a/src/server/rater/HttpRater.js b/src/server/rater/HttpRater.js index 28f0095..ab7e600 100644 --- a/src/server/rater/HttpRater.js +++ b/src/server/rater/HttpRater.js @@ -22,18 +22,14 @@ * "HttpRater" */ -var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), - - querystring = require( 'querystring' ) -; +var Class = require( 'easejs' ).Class; +var querystring = require( 'querystring' ); /** * Rates using one of the PHP raters */ module.exports = Class( 'HttpRater' ) - .implement( Rater ) .extend( { /** diff --git a/src/server/rater/ProcessManager.d.ts b/src/server/rater/ProcessManager.d.ts new file mode 100644 index 0000000..23d6705 --- /dev/null +++ b/src/server/rater/ProcessManager.d.ts @@ -0,0 +1,35 @@ +/** + * Rating process manager + * + * 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 . + */ + +import { Rater } from "./Rater"; + + +export declare class ProcessManager +{ + /** + * Returns the rater associated with the given id + * + * @param id - rater id + * + * @return requested rater + */ + byId( id: string ): Rater; +} diff --git a/src/server/rater/ProcessManager.js b/src/server/rater/ProcessManager.js index a5a67b1..13c7fe2 100644 --- a/src/server/rater/ProcessManager.js +++ b/src/server/rater/ProcessManager.js @@ -69,6 +69,9 @@ const _signum = { * * Handles formatting and sending requests to the rating process; and * processing replies. + * + * TODO: Rename this class and provide a more generic interface. The caller + * does not care that this sends data to another process for rating. */ module.exports = Class( 'ProcessManager', { diff --git a/src/server/rater/Rater.d.ts b/src/server/rater/Rater.d.ts new file mode 100644 index 0000000..d9dcb1b --- /dev/null +++ b/src/server/rater/Rater.d.ts @@ -0,0 +1,121 @@ +/** + * Contains Rater interface + * + * 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 . + */ + +import { ClientActions } from "../../client/action/ClientAction"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; +import { UserSession } from "../request/UserSession"; + + + +/** Result of rating */ +export interface RateResult +{ + /** Whether all suppliers were not able to provide rates */ + _unavailable_all: '0' | '1'; + + /** Result data */ + [P: string]: any; +} + + +/** + * Result of rating with an individual supplier + * + * This gets combined into a single RateResult prefixed with each supplier + * id and an underscore. + */ +export interface SupplierRateResult +{ + /** Rating worksheet data */ + __worksheet?: WorksheetData, + + /** Classification system results */ + __classes?: ClassificationData, + + /** Basic profiling data */ + __perf: PerformanceData, + + /** Ineligible message, if any */ + ineligible: string, + + /** Submit message, if any */ + submit: string, + + /** Final premium */ + premium: number, + + /** Rating data */ + [P: string]: any; +} + + +/** Basic profiling data */ +export interface PerformanceData +{ + /** Timestamp of beginning of rating */ + start: UnixTimestampMillis; + + /** Timestamp of end of rating */ + end: UnixTimestampMillis; + + /** Total rating time */ + total: Milliseconds; +} + + +/** + * Worksheet data from rater + * + * These data come from the compiled raters. + * + * TODO: Fill in a schema here + */ +export type WorksheetData = Record; + + +/** Classification results */ +export type ClassificationData = Record; + + +/** + * Represents a rater that will generate a quote from a given set of values + */ +export interface Rater +{ + /** + * Asynchronously performs rating + * + * @param quote - quote to perform rating on + * @param session - user session + * @param indv - individual supplier to rate (otherwise empty) + * @param success - continuation when rating is successful + * @param failure - continuation when rating fails + * + * @return self + */ + rate( + quote: ServerSideQuote, + session: UserSession, + indv: string, + success: ( rate_data: RateResult, actions: ClientActions ) => void, + failure: ( message: string ) => void, + ): this; +} diff --git a/src/server/rater/Rater.js b/src/server/request/UserRequest.d.ts similarity index 63% rename from src/server/rater/Rater.js rename to src/server/request/UserRequest.d.ts index 3647960..3449963 100644 --- a/src/server/rater/Rater.js +++ b/src/server/request/UserRequest.d.ts @@ -1,5 +1,5 @@ /** - * Contains Rater interface + * User request abstraction * * Copyright (C) 2010-2019 R-T Specialty, LLC. * @@ -19,22 +19,18 @@ * along with this program. If not, see . */ -var Interface = require( 'easejs' ).Interface; +import { UserSession } from "./UserSession"; /** - * Represents a rater that will generate a quote from a given set of values + * Representation of request from user */ -module.exports = Interface( 'Rater', +export declare class UserRequest { /** - * Asynchronously performs rating using the data from the given bucket + * Retrieve the current session * - * @param {Quote} quote to rate - * @param {function()} callback function to call when complete - * - * @return {Rater} self + * @return current session */ - 'public rate': [ 'quote', 'args', 'callback' ], -} ); - + getSession(): UserSession; +} diff --git a/src/server/request/UserResponse.d.ts b/src/server/request/UserResponse.d.ts new file mode 100644 index 0000000..7ba776f --- /dev/null +++ b/src/server/request/UserResponse.d.ts @@ -0,0 +1,28 @@ +/** + * UserResponse class + * + * 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 . + */ + + +/** + * Manipulates response to user request + */ +export declare class UserResponse +{ +} diff --git a/src/server/request/UserSession.d.ts b/src/server/request/UserSession.d.ts new file mode 100644 index 0000000..01937d7 --- /dev/null +++ b/src/server/request/UserSession.d.ts @@ -0,0 +1,34 @@ +/** + * UserSession class + * + * 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 . + */ + + +/** + * Session management + */ +export declare class UserSession +{ + /** + * Whether the user is logged in as an internal user + * + * @return true if internal user, otherwise false + */ + isInternal(): boolean; +} diff --git a/src/server/service/RatingService.js b/src/server/service/RatingService.js deleted file mode 100644 index aa9e53f..0000000 --- a/src/server/service/RatingService.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * 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 . - */ - -var Class = require( 'easejs' ).Class; - - -/** - * XXX: Half-assed, quick refactoring to extract from Server class; this is not - * yet complete! - * - * TODO: Logging should be implemented by observers - */ -module.exports = Class( 'RatingService', -{ - logger: null, - - dao: null, - - _server: null, - - _raters: null, - - - __construct: function( logger, dao, server, raters ) - { - this._logger = logger; - this._dao = dao; - this._server = server; - this._raters = raters; - }, - - - /** - * Sends rates to the client - * - * Note that the continuation will be called after all data saving is - * complete; the request will be sent back to the client before then. - * - * @param {UserRequest} request user request to satisfy - * @param {UserResponse} response pending response - * @param {Quote} quote quote to export - * @param {string} cmd applicable of command request - * @param {Function} callback continuation after saving is complete - * - * @return Server self to allow for method chaining - */ - 'public request': function( request, response, quote, cmd, callback ) - { - // cmd represents a request for a single rater - if ( !cmd && this._isQuoteValid( quote ) ) - { - // send an empty reply (keeps what is currently in the bucket) - this._server.sendResponse( request, quote, { - data: {}, - }, [] ); - - callback(); - return this; - } - - var program = quote.getProgram(); - - try - { - this._performRating( request, program, quote, cmd, callback ); - } - catch ( err ) - { - this._sendRatingError( request, quote, program, err ); - callback(); - } - - return this; - }, - - - _getProgramRater: function( program, quote ) - { - var rater = this._raters.byId( program.getId() ); - - // if a rater could not be found, we can't do any rating - if ( rater === null ) - { - this._logger.log( this._logger.PRIORITY_ERROR, - "Rating for quote %d (program %s) failed; missing module", - quote.getId(), - program.getId() - ); - } - - return rater; - }, - - - _isQuoteValid: function( quote ) - { - // 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; - }, - - - _performRating: function( request, program, quote, indv, c ) - { - var _self = this; - - var rater = this._getProgramRater( program ); - if ( !rater ) - { - this._server.sendError( request, 'Unable to perform rating.' ); - c(); - } - - this._logger.log( this._logger.PRIORITY_INFO, - "Performing '%s' rating for quote #%s", - quote.getProgramId(), - quote.getId() - ); - - rater.rate( quote, request.getSession(), indv, - function( rate_data, actions ) - { - actions = actions || []; - - _self.postProcessRaterData( - request, rate_data, actions, program, quote - ); - - const class_dest = {}; - - const cleaned = _self._cleanRateData( - rate_data, - class_dest - ); - - // TODO: move me during refactoring - _self._dao.saveQuoteClasses( quote, class_dest ); - - // save all data server-side (important: do after - // post-processing); async - _self._saveRatingData( quote, rate_data, indv, function() - { - // we're done - c(); - } ); - - // no need to wait for the save; send the response - _self._server.sendResponse( request, quote, { - data: cleaned, - initialRatedDate: quote.getRatedDate(), - lastRatedDate: quote.getLastPremiumDate() - }, actions ); - }, - function( message ) - { - _self._sendRatingError( request, quote, program, - Error( message ) - ); - - c(); - } - ); - }, - - - /** - * 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 quote to save data to - * @param {Object} data rating data - * - * @return {undefined} - */ - _saveRatingData: function( quote, data, indv, c ) - { - // only update the last premium calc date on the initial request - if ( !indv ) - { - var cur_date = Math.round( - ( new Date() ).getTime() / 1000 - ); - - quote.setLastPremiumDate( cur_date ); - quote.setRatedDate( cur_date ); - - function done() - { - c(); - } - - // 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, done, done, {} ); - } - 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 {UserRequest} request user request to satisfy - * @param {Object} data rating data returned - * @param {Array} actions actions to send to client - * @param {Program} program program used to perform rating - * @param {Quote} quote quote used for rating - * - * @return {undefined} - */ - 'virtual protected postProcessRaterData': function( - request, data, actions, program, 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 ); - - 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( ',' ) - ); - } - }, - - - _sendRatingError: function( request, quote, program, err ) - { - // well that's no good - this._logger.log( this._logger.PRIORITY_ERROR, - "Rating for quote %d (program %s) failed: %s", - quote.getId(), - program.getId(), - 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() ) - ? '

[Internal] ' + err.message + '

' + - '
' + err.stack.replace( /\n/g, '
' ) - : '' - ) - ); - }, - - - _processWorksheetData: function( qid, data ) - { - // TODO: this should be done earlier on, so that this is not necessary - var wre = /^(.+)___worksheet$/, - 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 ]; - } - } - - var _self = this; - this._dao.setWorksheets( qid, worksheets, function( err ) - { - if ( err ) - { - _self._logger.log( this._logger.PRIORITY_ERROR, - "Failed to save rating worksheets for quote %d", - quote.getId(), - err.message + '\n-!' + err.stack.replace( /\n/g, '\n-!' ) - ); - } - } ); - }, - - - serveWorksheet: function( request, quote, supplier, index ) - { - var qid = quote.getId(), - _self = this; - - this._dao.getWorksheet( qid, supplier, index, function( data ) - { - _self._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 {Object} data rate data - * - * @return {Object} modified rate data - */ - 'private _cleanRateData': function( data, classes ) - { - classes = classes || {}; - - 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; - }, -} ); - diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts new file mode 100644 index 0000000..df27f31 --- /dev/null +++ b/src/server/service/RatingService.ts @@ -0,0 +1,502 @@ +/** + * 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 . + */ + +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"; + +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 + */ + constructor( + private readonly _logger: PriorityLog, + private readonly _dao: ServerDao, + private readonly _server: Server, + private readonly _rater_manager: ProcessManager, + ) {} + + + /** + * TODO: Remove once traits subtypes are converted to TS + * + * This works around an easejs bug where prototype constructors are not + * properly invoked. Note that technically the constructor above is + * invoked twice by easejs: once with no arguments, and again when + * calling this method with the proper arguments. + */ + __construct() + { + (RatingService ).apply( this, arguments ); + } + + + /** + * 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 + { + return new Promise( resolve => + { + // cmd represents a request for a single rater + if ( !cmd && this._isQuoteValid( quote ) ) + { + // send an empty reply (keeps what is currently in the + // bucket) + this._server.sendResponse( request, quote, { + data: {}, + }, [] ); + + // 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: 0, + lastRatedDate: 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 + { + return new Promise( ( 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, () => {}, () => {} + ); + + const result = { + data: cleaned, + initialRatedDate: quote.getRatedDate(), + lastRatedDate: quote.getLastPremiumDate() + }; + + // save all data server-side (important: do after + // post-processing); async + this._saveRatingData( quote, rate_data, indv, function() + { + // we're done + resolve( result ); + } ); + + // no need to wait for the save; send the response + this._server.sendResponse( request, quote, result, actions ); + }, + ( 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 = Math.round( + ( new Date() ).getTime() / 1000 + ); + + quote.setLastPremiumDate( cur_date ); + quote.setRatedDate( 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, { + ratedata: 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', after: 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() ) + ? '

[Internal] ' + err.message + '

' + + '
' + ( err.stack || "" ).replace( /\n/g, '
' ) + : '' + ) + ); + } + + + /** + * 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 = {}; + + // 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: Error | null ) => + { + 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 = {}; + + // 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; + } +} diff --git a/src/server/service/RatingServicePublish.js b/src/server/service/RatingServicePublish.js index fd5a539..ce843f0 100644 --- a/src/server/service/RatingServicePublish.js +++ b/src/server/service/RatingServicePublish.js @@ -21,8 +21,8 @@ 'use strict'; -const { Trait } = require( 'easejs' ); -const RatingService = require( './RatingService' ); +const { Interface, Trait } = require( 'easejs' ); +const { RatingService } = require( './RatingService' ); /** @@ -51,7 +51,8 @@ const RatingService = require( './RatingService' ); * See the body of `#_sendMessage' for their values. */ module.exports = Trait( 'RatingServicePublish' ) - .extend( RatingService, + .implement( Interface( { 'postProcessRaterData': [] } ) ) + .extend( { /** * AMQP library (amqplib API) @@ -75,7 +76,7 @@ module.exports = Trait( 'RatingServicePublish' ) * * @type {DebugLog} */ - 'private _logger': null, + 'private _log': null, /** @@ -87,9 +88,9 @@ module.exports = Trait( 'RatingServicePublish' ) */ __mixin( amqp, conf, logger ) { - this._amqp = amqp; - this._conf = conf; - this._logger = logger; + this._amqp = amqp; + this._conf = conf; + this._log = logger; }, @@ -104,7 +105,7 @@ module.exports = Trait( 'RatingServicePublish' ) * * @return {undefined} */ - 'override protected postProcessRaterData'( + 'abstract override postProcessRaterData'( request, data, actions, program, quote ) { @@ -127,13 +128,13 @@ module.exports = Trait( 'RatingServicePublish' ) quote ); } ) - .then( () => this._logger.log( - this._logger.PRIORITY_INFO, + .then( () => this._log.log( + this._log.PRIORITY_INFO, "Published quote " + quote.getId() + " to post-rate exchange '" + exchange + "'" ) ) - .catch( e => this._logger.log( - this._logger.PRIORITY_ERROR, + .catch( e => this._log.log( + this._log.PRIORITY_ERROR, "Post-rate exchange publish failure for quote " + quote.getId() + ": " + e.message ) ); diff --git a/src/server/service/RatingServiceSubmitNotify.js b/src/server/service/RatingServiceSubmitNotify.js deleted file mode 100644 index 05712c6..0000000 --- a/src/server/service/RatingServiceSubmitNotify.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Notification on all submit - * - * Copyright (C) 2010-2019 R-T Specialty, LLC. - * - * This file is part of liza. - * - * liza is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License - * along with this program. If not, see . - */ - -'use strict'; - -const { Trait } = require( 'easejs' ); -const DslRaterContext = require( '../rater/DslRaterContext' ) -const RatingService = require( './RatingService' ); - - -/** - * Triggers DataApi when no results are available - * - * This information is currently stored in `__prem_avail_count`. In the - * future, it may be worth accepting a parameter to configure this at - * runtime. - * - * Notification status will persist using the provided DAO. The next time - * such a notification is requested, it will only occur if the flag is not - * set. The flag is not set in the event of an error (determined by the - * DataApi; usually an HTTP error). - */ -module.exports = Trait( 'RatingServiceSubmitNotify' ) - .extend( RatingService, -{ - /** - * Function returning DataApi to trigger - * @type {Function(UserSession):DataApi} - */ - 'private _dapif': null, - - /** - * Data store for notification flag - * @type {ServerDao} - */ - 'private _notifyDao': null, - - - /** - * Initialize mixin with DataApi to trigger - * - * @param {Function(UserSession):DataApi} dapif Function producing DataApi - * @param {ServerDao} dao store for notification flag - */ - __mixin( dapif, dao ) - { - this._dapif = dapif; - this._notifyDao = dao; - }, - - - /** - * Trigger previously provided DataApi when no results are available - * - * Result count is determined by DATA.__prem_avail_count. If the - * notification is successful (determined by the DataApi), then a - * flag will be set preventing the request from being trigerred for - * subsequent rating data. - * - * @param {UserRequest} request user request - * @param {Object} data rating data returned - * @param {Array} actions actions to send to client - * @param {Program} program program used to perform rating - * @param {Quote} quote quote used for rating - * - * @return {undefined} - */ - 'override protected postProcessRaterData'( - request, data, actions, program, quote - ) - { - const quote_id = quote.getId(); - const avail = ( data.__prem_avail_count || [ 0 ] )[ 0 ]; - - if ( avail === 0 ) - { - this._maybeNotify( quote_id, request ); - } - - this.__super( request, data, actions, program, quote ); - }, - - - /** - * Perform notification if flag has not been set - * - * See #postProcessRaterData for more information. - * - * @param {number} quote_id effective quote/document id - * @param {UserRequest} request user request - * - * @return {undefined} - */ - 'private _maybeNotify'( quote_id, request ) - { - this._getNotifyState( quote_id, notified => - { - if ( notified === true ) - { - return; - } - - // make the request, only setting the notification flag if - // it is successful - this._dapif( request ) - .request( { quote_id: quote_id }, err => - { - err || this._setNotified( quote_id ); - } ); - } ); - }, - - - /** - * Get value of notification flag - * - * @param {number} quote_id id of quote - * @param {function(boolean)} callback callback to call when complete - * - * @return {undefined} - */ - 'private _getNotifyState'( quote_id, callback ) - { - this._notifyDao.getDocumentField( - quote_id, - 'submitNotified', - ( err, value ) => callback( value ) - ); - }, - - - /** - * Set notification flag - * - * @param {number} quote_id id of quote - * - * @return {undefined} - */ - 'private _setNotified'( quote_id ) - { - this._notifyDao.setDocumentField( - quote_id, 'submitNotified', true - ); - }, -} ); diff --git a/src/test/server/service/RatingServiceStub.js b/src/test/server/service/RatingServiceStub.js deleted file mode 100644 index 4d2f285..0000000 --- a/src/test/server/service/RatingServiceStub.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Tests RatingService - * - * 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 . - */ - -'use strict' - - -exports.getStubs = function() -{ - const program_id = 'foo'; - const program = { - getId: () => program_id, - }; - - // rate reply - const stub_rate_data = {}; - - const rater = { - rate: ( quote, session, indv, callback ) => callback( stub_rate_data ), - }; - - const raters = { - byId: () => rater, - }; - - const logger = { - log: () => {}, - }; - - const server = { - sendResponse: () => {}, - sendError: () => {}, - }; - - const dao = { - mergeBucket: () => {}, - saveQuoteClasses: () => {}, - setWorksheets: () => {}, - saveQuote: () => {}, - }; - - const session = { - isInternal: () => false, - }; - - const request = { - getSession: () => session, - getSessionIdName: () => {}, - }; - const response = {}; - - const quote = { - getProgramId: () => program_id, - getProgram: () => program, - getId: () => 0, - setLastPremiumDate: () => {}, - setRatedDate: () => {}, - getRatedDate: () => 0, - getLastPremiumDate: () => 0 - }; - - return { - program: program, - stub_rate_data: stub_rate_data, - rater: rater, - raters: raters, - logger: logger, - server: server, - dao: dao, - session: session, - request: request, - response: response, - quote: quote, - }; -}; diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts index 83b11b5..5572739 100644 --- a/src/types/misc.d.ts +++ b/src/types/misc.d.ts @@ -38,12 +38,24 @@ type NominalType = K & { __nominal_type__: T }; +/** Unit of time in seconds */ +type Seconds = NominalType; + + /** * Unix timestamp * * Number of seconds since the Unix epoch (1970-01-01 UTC). */ -type UnixTimestamp = NominalType; +type UnixTimestamp = NominalType; + + +/** Unit of time in milliseconds */ +type Milliseconds = NominalType; + + +/** Unix timestamp represented in milliseconds */ +type UnixTimestampMillis = NominalType; /** diff --git a/test/numeric-test.ts b/test/numeric-test.ts new file mode 100644 index 0000000..8f07bb6 --- /dev/null +++ b/test/numeric-test.ts @@ -0,0 +1,59 @@ +/** + * Test numeric types + * + * 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 . + */ + +import { expect } from 'chai'; +import { PositiveInteger, isPositiveInteger } from "../src/numeric"; + + +describe( 'isPositiveInteger', () => +{ + [ + 0, + 5, + ].forEach( value => it( `accepts positive integers (${value})`, () => + { + expect( isPositiveInteger( value ) ).to.be.true; + } ) ); + + + [ + -1, + -5, + ].forEach( value => it( `rejects negative integers (${value})`, () => + { + expect( isPositiveInteger( value ) ).to.be.false; + } ) ); + + + it( "asserts type PositiveInteger", () => + { + const n = 5; + + if ( isPositiveInteger( n ) ) + { + // TS should recognize as PositiveInteger within this block + checkPositiveInteger( n ); + } + } ); +} ); + + +const checkPositiveInteger = ( _n: PositiveInteger ): void => {}; diff --git a/test/server/service/RatingServiceSubmitNotifyTest.js b/test/server/service/RatingServiceSubmitNotifyTest.js deleted file mode 100644 index 1333eaf..0000000 --- a/test/server/service/RatingServiceSubmitNotifyTest.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Tests RatingServiceSubmitNotify - * - * 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 . - */ - -'use strict' - -const { Class } = require( 'easejs' ); -const { expect } = require( 'chai' ); - - -const { - dapi: { - DataApi, - }, - server: { - service: { - RatingServiceSubmitNotify: Sut, - RatingService, - }, - }, - test: { - server: { - service: { - RatingServiceStub, - }, - }, - }, -} = require( '../../../' ); - - -describe( 'RatingServiceSubmitNotify', () => -{ - [ - // not available; make successful request and save flag - { - prem_avail_count: [ 0 ], - prev_called: false, - expected_request: true, - request_err: null, - save: true, - }, - // not available; make failing request, don't save flag - { - prem_avail_count: [ 0 ], - prev_called: false, - expected_request: true, - request_err: Error(), - save: false, - }, - // available - { - prem_avail_count: [ 2 ], - prev_called: false, - expected_request: false, - request_err: null, - save: false, - }, - // this shouldn't happen; ignore all but first index - { - prem_avail_count: [ 2, 2 ], - prev_called: false, - expected_request: false, - request_err: null, - save: false, - }, - // save as above, but already saved - { - prem_avail_count: [ 0 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - // available; don't make request - { - prem_avail_count: [ 2 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - // this shouldn't happen; ignore all but first index - { - prem_avail_count: [ 2, 2 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - ].forEach( ( expected, i ) => - it( `sends request on post process if no premiums (#${i})`, done => - { - const { - dao, - logger, - quote, - raters, - request, - response, - server, - stub_rate_data, - } = RatingServiceStub.getStubs(); - - const quote_id = 1234; - let requested = false; - - const dapif = given_request => - Class.implement( DataApi ).extend( - { - // warning: if an expectation fails, because of how - // RatingService handles errors, it will cause the test to - // _hang_ rather than throw the assertion error - request( data, callback, id ) - { - expect( given_request ).to.equal( request ); - expect( data ).to.deep.equal( { quote_id: quote_id } ); - - requested = true; - - callback( expected.request_err, null ); - }, - } )(); - - const sut = RatingService.use( Sut( dapif, dao ) )( - logger, dao, server, raters - ); - - quote.getId = () => quote_id; - - // one of the methods that is called by the supertype - let save_called = false; - dao.setWorksheets = () => save_called = true; - - // whether the notify flag is actually set - let notify_saved = false; - - // request for notification status - dao.getDocumentField = ( qid, key, callback ) => - { - expect( qid ).to.equal( quote_id ); - expect( key ).to.equal( 'submitNotified' ); - - callback( expected.flag_error, expected.prev_called ); - }; - - dao.setDocumentField = ( qid, key, value, callback ) => - { - expect( qid ).to.equal( quote_id ); - expect( key ).to.equal( 'submitNotified' ); - expect( value ).to.equal( true ); - - notify_saved = true; - }; - - stub_rate_data.__prem_avail_count = expected.prem_avail_count; - - sut.request( request, response, quote, 'something', () => - { - expect( requested ).to.equal( expected.expected_request ); - expect( save_called ).to.be.true; - - // only save notification status if we're notifying - expect( notify_saved ).to.equal( expected.save ); - - done(); - } ); - } ) - ); -} ); diff --git a/test/server/service/RatingServiceTest.js b/test/server/service/RatingServiceTest.js deleted file mode 100644 index b04c354..0000000 --- a/test/server/service/RatingServiceTest.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Tests RatingService - * - * 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 . - */ - -'use strict' - -const { expect } = require( 'chai' ); -const Sut = require( '../../../' ).server.service.RatingService; -const RatingServiceStub = require( '../../../' ).test.server.service.RatingServiceStub; - -describe( 'RatingService', () => -{ - describe( "protected API", () => - { - it( "calls #postProcessRaterData after rating before save", done => - { - let processed = false; - - const { - logger, - server, - raters, - dao, - request, - response, - quote, - } = RatingServiceStub.getStubs(); - - dao.mergeBucket = () => - { - expect( processed ).to.equal( true ); - done(); - }; - - const sut = Sut.extend( - { - 'override postProcessRaterData'( - request, data, actions, program, quote - ) - { - processed = true; - } - } )( logger, dao, server, raters ); - - sut.request( request, response, quote, 'something', () => {} ); - } ); - - it( "calls getLastPremiumDate during #_performRating", done => - { - let getLastPremiumDateCallCount = 0; - - const last_date = 1234; - const initial_date = 2345; - - const { - logger, - server, - raters, - dao, - request, - response, - quote, - } = RatingServiceStub.getStubs(); - - quote.getLastPremiumDate = () => - { - getLastPremiumDateCallCount++; - return last_date - }; - - quote.getRatedDate = () => initial_date; - - const sut = Sut( logger, dao, server, raters ); - - server.sendResponse = ( request, quote, resp, actions ) => - { - expect( getLastPremiumDateCallCount ).to.equal( 2 ); - expect( resp.initialRatedDate ).to.equal( initial_date ); - expect( resp.lastRatedDate ).to.equal( last_date ); - - done(); - }; - - sut.request( request, response, quote, null, () => {} ); - } ); - - } ); -} ); diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts new file mode 100644 index 0000000..fd659d6 --- /dev/null +++ b/test/server/service/RatingServiceTest.ts @@ -0,0 +1,411 @@ +/** + * Tests RatingService + * + * 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 . + */ + +import { RatingService as Sut } from "../../../src/server/service/RatingService"; + +import { ClientActions } from "../../../src/client/action/ClientAction"; +import { PriorityLog } from "../../../src/server/log/PriorityLog"; +import { ProcessManager } from "../../../src/server/rater/ProcessManager"; +import { Program } from "../../../src/program/Program"; +import { QuoteId } from "../../../src/quote/Quote"; +import { Rater, RateResult } from "../../../src/server/rater/Rater"; +import { Server } from "../../../src/server/Server"; +import { ServerSideQuote } from "../../../src/server/quote/ServerSideQuote"; +import { UserRequest } from "../../../src/server/request/UserRequest"; +import { UserResponse } from "../../../src/server/request/UserResponse"; +import { UserSession } from "../../../src/server/request/UserSession"; + +import { + ServerDao, + Callback as ServerDaoCallback +} from "../../../src/server/db/ServerDao"; + +import { expect, use as chai_use } from 'chai'; +chai_use( require( 'chai-as-promised' ) ); + + +describe( 'RatingService', () => +{ + it( "returns rating results", () => + { + const { + logger, + server, + raters, + dao, + request, + response, + quote, + stub_rate_data, + } = getStubs(); + + const sut = new Sut( logger, dao, server, raters ); + + const expected = { + data: stub_rate_data, + initialRatedDate: quote.getRatedDate(), + lastRatedDate: quote.getLastPremiumDate(), + }; + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.deep.equal( expected ); + } ); + + it( "saves rate data to own field", () => + { + const { + logger, + server, + raters, + dao, + request, + response, + quote, + stub_rate_data, + } = getStubs(); + + let saved_rates = false; + + dao.saveQuote = ( + quote: ServerSideQuote, + success: ServerDaoCallback, + _failure: ServerDaoCallback, + save_data: Record, + ) => + { + expect( save_data ).to.deep.equal( { + ratedata: stub_rate_data, + } ); + + saved_rates = true; + success( quote ); + + return dao; + }; + + const sut = new Sut( logger, dao, server, raters ); + + return sut.request( request, response, quote, "" ) + .then( () => + { + expect( saved_rates ).to.be.true; + } ); + } ); + + + it( "rejects and responds with error", () => + { + const { + dao, + logger, + program, + quote, + rater, + raters, + request, + response, + server, + } = getStubs(); + + const expected_error = new Error( "expected error" ); + + rater.rate = () => { throw expected_error; }; + + const sut = new Sut( logger, dao, server, raters ); + + let logged = false; + + logger.log = function( + priority: number, + _format: string, + qid: QuoteId, + program_id: string, + message: string, + ) + { + if ( typeof message === 'string' ) + { + expect( priority ).to.equal( logger.PRIORITY_ERROR ); + expect( qid ).to.equal( quote.getId() ); + expect( program_id ).to.equal( program.getId() ); + expect( message ).to.contain( expected_error.message ); + + logged = true; + } + + return logger; + }; + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.rejectedWith( expected_error ) + .then( () => expect( logged ).to.be.true ); + } ); + + + it( "returns error message from rater", () => + { + const { + dao, + logger, + quote, + rater, + raters, + request, + response, + server, + } = getStubs(); + + const expected_message = 'expected foo'; + + const sut = new Sut( logger, dao, server, raters ); + + rater.rate = ( + _quote: ServerSideQuote, + _session: UserSession, + _indv: string, + _success: ( data: RateResult, actions: ClientActions ) => void, + failure: ( message: string ) => void, + ) => + { + failure( expected_message ); + return rater; + }; + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.rejectedWith( Error, expected_message ); + } ); + + + describe( "protected API", () => + { + it( "calls #postProcessRaterData after rating before save", done => + { + let processed = false; + + const { + logger, + server, + raters, + dao, + request, + response, + quote, + } = getStubs(); + + dao.mergeBucket = () => + { + expect( processed ).to.equal( true ); + done(); + + return dao; + }; + + const sut = new class extends Sut + { + postProcessRaterData() + { + processed = true; + } + }( logger, dao, server, raters ); + + sut.request( request, response, quote, 'something' ); + } ); + + it( "calls getLastPremiumDate during #_performRating", done => + { + let getLastPremiumDateCallCount = 0; + + const last_date = 1234; + const initial_date = 2345; + + const { + logger, + server, + raters, + dao, + request, + response, + quote, + } = getStubs(); + + quote.getLastPremiumDate = () => + { + getLastPremiumDateCallCount++; + return last_date + }; + + quote.getRatedDate = () => initial_date; + + const sut = new Sut( logger, dao, server, raters ); + + server.sendResponse = ( _request: any, _quote: any, resp: any, _actions: any ) => + { + expect( getLastPremiumDateCallCount ).to.equal( 2 ); + expect( resp.initialRatedDate ).to.equal( initial_date ); + expect( resp.lastRatedDate ).to.equal( last_date ); + + done(); + + return server; + }; + + sut.request( request, response, quote, "" ); + } ); + } ); +} ); + + +function getStubs() +{ + const program_id = 'foo'; + + const program = { + getId: () => program_id, + ineligibleLockCount: 0, + }; + + // rate reply + const stub_rate_data: RateResult = { + _unavailable_all: '0', + }; + + const rater = new class implements Rater + { + rate( + _quote: ServerSideQuote, + _session: UserSession, + _indv: string, + success: ( data: RateResult, actions: ClientActions ) => void, + _failure: ( message: string ) => void, + ) + { + // force to be async so that the tests resemble how the code + // actually runs + process.nextTick( () => success( stub_rate_data, [] ) ); + + return this; + } + }; + + const raters = { + byId: () => rater, + }; + + const logger = new class implements PriorityLog + { + readonly PRIORITY_ERROR: number = 0; + readonly PRIORITY_IMPORTANT: number = 1; + readonly PRIORITY_DB: number = 2; + readonly PRIORITY_INFO: number = 3; + readonly PRIORITY_SOCKET: number = 4; + + log( _priority: number, ..._args: Array ): this + { + return this; + } + }; + + const server = { + sendResponse: () => server, + sendError: () => server, + }; + + const dao = new class implements ServerDao + { + saveQuote( + quote: ServerSideQuote, + success: ServerDaoCallback, + _failure: ServerDaoCallback, + _save_data: Record, + ): this + { + success( quote ); + return this; + } + + mergeBucket(): this + { + return this; + } + + saveQuoteClasses(): this + { + return this; + } + + setWorksheets(): this + { + return this; + } + + saveQuoteState(): this + { + throw new Error( "Unused method" ); + } + + saveQuoteLockState(): this + { + throw new Error( "Unused method" ); + } + + getWorksheet(): this + { + throw new Error( "Unused method" ); + } + }; + + const session = { + isInternal: () => false, + }; + + const request = { + getSession: () => session, + getSessionIdName: () => {}, + }; + + const response = {}; + + const quote = { + getProgramId: () => program_id, + getProgram: () => program, + getId: () => 0, + setLastPremiumDate: () => quote, + setRatedDate: () => quote, + getRatedDate: () => 0, + getLastPremiumDate: () => 0, + getCurrentStepId: () => 0, + setExplicitLock: () => quote, + }; + + return { + program: program, + stub_rate_data: stub_rate_data, + rater: rater, + raters: raters, + logger: logger, + server: server, + dao: dao, + session: session, + request: request, + response: response, + quote: quote, + }; +};