' ).html(
- "The quote is locked and cannot be modified."
- )
+ $( '
' ).html( lock_msg )
)
);
}, 25 );
diff --git a/src/quote/BaseQuote.js b/src/quote/BaseQuote.js
index 05df67b..471a79f 100644
--- a/src/quote/BaseQuote.js
+++ b/src/quote/BaseQuote.js
@@ -26,7 +26,6 @@ var Class = require( 'easejs' ).Class,
Program = require( '../program/Program' ).Program,
EventEmitter = require( 'events' ).EventEmitter;
-
/**
* Creates a new quote
*
@@ -148,8 +147,8 @@ module.exports = Class( 'BaseQuote' )
*/
'public __construct': function( id, bucket )
{
- this._id = id;
- this._bucket = bucket;
+ this._id = id;
+ this._bucket = bucket;
},
@@ -271,6 +270,71 @@ module.exports = Class( 'BaseQuote' )
},
+ /**
+ * Returns the quote's expiration date
+ *
+ * @return {number} quote's initial rated date
+ */
+ 'public getExpirationDate': function()
+ {
+ var post_rate = ( this._initialRatedDate > 0 );
+
+ // Don't attempt to calculate expiration date if expiration is not defined
+ if ( !this._program
+ || !this._program.lockTimeout
+ || ( !post_rate && !this._program.lockTimeout.preRateExpiration )
+ || ( post_rate && !this._program.lockTimeout.postRateExpiration ))
+ {
+ return Number.POSITIVE_INFINITY;
+ }
+
+ var reference_date = ( post_rate ) ? this._initialRatedDate : this._startDate;
+ var expiration_period = ( post_rate )
+ ? this._program.lockTimeout.postRateExpiration
+ : this._program.lockTimeout.preRateExpiration;
+
+ // Use Date.setDate to accommodate leap seconds, leap years, DST, etc.
+ var expiration_date = new Date( reference_date );
+ expiration_date.setDate( expiration_date.getDate() + expiration_period );
+
+ return expiration_date.getTime();
+ },
+
+
+ /**
+ * Returns whether the quote has expired or not
+ *
+ * @param {Date} current_date current date to determine if expiration date has passed
+ *
+ * @return {boolean} flag indicating if the quote has expired
+ */
+ 'public hasExpired': function( current_date )
+ {
+ var timeout = ( this._program && this._program.lockTimeout )
+ ? this._program.lockTimeout
+ : { preRateGracePeriod: 0, postRateGracePeriod: 0 };
+
+ var grace_period = ( this._initialRatedDate > 0 )
+ ? ( timeout.postRateGracePeriod || 0 )
+ : ( timeout.preRateGracePeriod || 0 );
+
+ var expiration_timestamp = this.getExpirationDate();
+
+ // If the timestamp is INFINITY, the quote will never expire
+ // NOTE: The Date constructor does not support INFINITY as the timestamp
+ if ( expiration_timestamp === Number.POSITIVE_INFINITY )
+ {
+ return false;
+ }
+
+ // Use Date.setDate to accommodate leap seconds, leap years, DST, etc.
+ var expiration_date = new Date( expiration_timestamp );
+ expiration_date.setDate( expiration_date.getDate() + grace_period );
+
+ return current_date.getTime() > expiration_date.getTime();
+ },
+
+
/**
* Sets id of agent that owns the quote
*
@@ -681,7 +745,7 @@ module.exports = Class( 'BaseQuote' )
'public getExplicitLockReason': function()
{
return ( this.isBound() )
- ? 'Quote has been bound'
+ ? 'This quote has been bound and cannot be modified.'
: this._explicitLock;
},
diff --git a/src/server/Server.js b/src/server/Server.js
index 60a965c..d146ffd 100644
--- a/src/server/Server.js
+++ b/src/server/Server.js
@@ -255,6 +255,12 @@ module.exports = Class( 'Server' )
/**
* Initializes a quote with any existing quote data
*
+ * @param Integer quote_id id of the quote
+ * @param Program program program that the quote will be a part of
+ * @param Object request request to create quote
+ * @param Function( quote ) callback function to call when quote is ready
+ * @param Function( quote ) callback function to call when an error occurs
+ *
* @return Server self to allow for method chaining
*/
initQuote: function( quote, program, request, callback, error_callback )
@@ -696,6 +702,15 @@ module.exports = Class( 'Server' )
}
}
+ // Expire quote as needed
+ if ( quote.hasExpired( new Date() ) )
+ {
+ quote.setExplicitLock(
+ 'This quote has expired and cannot be modified. ' +
+ 'Please contact support with any questions.'
+ );
+ }
+
var bucket = quote.getBucket(),
lock = quote.getExplicitLockReason(),
lock_step = quote.getExplicitLockStep(),
@@ -722,7 +737,7 @@ module.exports = Class( 'Server' )
)
} ];
- lock = 'concurrent-access';
+ lock = 'Quote is locked due to concurrent access.';
}
// decrypt bucket contents, if necessary, and return
diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js
index 2206d5e..e23c670 100644
--- a/src/server/daemon/controller.js
+++ b/src/server/daemon/controller.js
@@ -633,9 +633,11 @@ function doRoute( program, request, data, resolve, reject )
/**
* Creates a new quote instance with the given quote id
*
- * @param Integer quote_id id of the quote
- * @param Program program program that the quote will be a part of
- * @param Function( quote ) callback function to call when quote is ready
+ * @param Integer quote_id id of the quote
+ * @param Program program program that the quote will be a part of
+ * @param Object request request to create quote
+ * @param Function( quote ) callback function to call when quote is ready
+ * @param Function( quote ) callback function to call when an error occurs
*
* @return undefined
*/
diff --git a/src/server/quote/ServerSideQuote.js b/src/server/quote/ServerSideQuote.js
index 3e6244b..8f35f42 100644
--- a/src/server/quote/ServerSideQuote.js
+++ b/src/server/quote/ServerSideQuote.js
@@ -178,6 +178,6 @@ module.exports = Class( 'ServerSideQuote' )
this._metabucket.setValues( data );
return this;
- },
+ }
} );
diff --git a/src/test/program/util.js b/src/test/program/util.js
index 00e49bf..7d7d30b 100644
--- a/src/test/program/util.js
+++ b/src/test/program/util.js
@@ -245,8 +245,29 @@ exports.stubCmatch = ( cmatch_dfn ) =>
return cmatch;
};
-
-exports.stubProgram = Program => Program.extend(
+/**
+ * Produce a stub class that extends the abstract Program class
+ *
+ * @param {Program} ProgramSut class to extend
+ * @param {function} mockInitQuote optional: mock implementation of 'initQuote' function
+ *
+ * @return {Class} stub class for creating mock Program object(s)
+ */
+exports.stubProgram = ( ProgramSut, mockInitQuote ) =>
{
- classifier: __dirname + '/DummyClassifier',
-} );
+ // TODO: Make ProgramSut optional
+ if ( !ProgramSut )
+ {
+ throw new Error( "Class for Program stub must be specified." );
+ }
+
+ mockInitQuote = mockInitQuote ||
+ ProgramSut.initQuote ||
+ ( ( bucket, store_only ) => {} );
+
+ return ProgramSut.extend(
+ {
+ classifier: __dirname + '/DummyClassifier',
+ initQuote: mockInitQuote
+ } );
+};
diff --git a/test/quote/BaseQuoteTest.js b/test/quote/BaseQuoteTest.js
index 9c6b8cd..f1ba018 100644
--- a/test/quote/BaseQuoteTest.js
+++ b/test/quote/BaseQuoteTest.js
@@ -21,38 +21,391 @@
'use strict';
-const chai = require( 'chai' );
-const expect = chai.expect;
-const { BaseQuote } = require( '../../' ).quote;
+const chai = require( 'chai' );
+const expect = chai.expect;
+const { BaseQuote } = require( '../../' ).quote;
+const Program = require( '../../src/program/Program' ).Program;
+const Util = require( '../../src/test/program/util' );
+
+const messages = {
+ expire: 'This quote has expired and cannot be modified. ' +
+ 'Please contact support with any questions.',
+ bound: 'This quote has been bound and cannot be modified.'
+};
describe( 'BaseQuote', () =>
{
- [
- {
- property: 'startDate',
- value: 12345
- },
- {
- property: 'initialRatedDate',
- value: 12345
- },
- {
- property: 'agentEntityId',
- value: 12434300
- },
- ].forEach( testCase =>
+ describe( 'accessors & mutators', () =>
{
- const quote = BaseQuote( 123, {} );
- const property = testCase.property;
- const title_cased = property.charAt( 0 ).toUpperCase() + property.slice( 1 );
- const setter = 'set' + title_cased;
- const getter = 'get' + title_cased;
+ [
+ {
+ property: 'startDate',
+ default: 0,
+ value: 946684800
+ },
+ {
+ property: 'initialRatedDate',
+ default: 0,
+ value: 946684800
+ },
+ {
+ property: 'agentId',
+ default: 0,
+ value: 12345678
+ },
+ {
+ property: 'agentEntityId',
+ default: 0,
+ value: 12345678
+ },
+ {
+ property: 'agentName',
+ default: '',
+ value: 'name'
+ },
+ {
+ property: 'imported',
+ default: false,
+ value: true,
+ accessor: 'is'
+ },
+ {
+ property: 'bound',
+ default: false,
+ value: true,
+ accessor: 'is'
+ },
+ {
+ property: 'currentStepId',
+ default: 1,
+ value: 2
+ },
+ {
+ property: 'topVisitedStepId',
+ default: 1,
+ value: 2
+ },
+ {
+ property: 'topSavedStepId',
+ value: 1
+ },
+ {
+ property: 'error',
+ default: '',
+ value: 'ERROR'
+ }
- it( property + ' can be mutated and accessed', () =>
+ ].forEach( testCase =>
{
- expect( quote[getter].call( null ) ).to.be.undefined;
- quote[setter].call( null, testCase.value );
- expect( quote[getter].call( null ) ).to.equal( testCase.value );
+ const quote = BaseQuote( 123, {} );
+ const property = testCase.property;
+ const title_cased = property.charAt( 0 ).toUpperCase() + property.slice( 1 );
+ const setter = ( testCase.mutator || 'set' ) + title_cased;
+ const getter = ( testCase.accessor || 'get' ) + title_cased;
+
+ it( property + ' can be mutated and accessed', () =>
+ {
+ expect( quote[ getter ].call( quote ) ).to.equal( testCase.default );
+ quote[ setter ].call( quote, testCase.value );
+ expect( quote[ getter ].call( quote ) ).to.equal( testCase.value );
+ } );
+ } );
+ } );
+
+ describe( 'locking mechanisms', () =>
+ {
+ [
+ {
+ description: 'default values',
+ reason: '',
+ step: 0,
+ bound: false,
+ imported: false,
+ locks: false
+ },
+ {
+ description: 'quote with a reason',
+ reason: 'reason',
+ step: 0,
+ bound: false,
+ imported: false,
+ locks: true
+ },
+ {
+ description: 'quote with a lock on step #2',
+ reason: '',
+ step: 2,
+ bound: false,
+ imported: false,
+ locks: false
+ },
+ {
+ description: 'quote with a reason and a lock on step #2',
+ reason: 'reason',
+ step: 2,
+ bound: false,
+ imported: false,
+ locks: false
+ },
+ {
+ description: 'bound quote',
+ reason: { given: '', expected: messages.bound },
+ step: 0,
+ bound: true,
+ imported: false,
+ locks: true
+ },
+ {
+ description: 'imported quote',
+ reason: '',
+ step: 0,
+ bound: false,
+ imported: true,
+ locks: true
+ },
+ {
+ description: 'bound and imported quote',
+ reason: { given: '', expected: messages.bound },
+ step: 0,
+ bound: true,
+ imported: true,
+ locks: true
+ },
+ {
+ description: 'bound quote with a lock on step #2',
+ reason: { given: '', expected: messages.bound },
+ step: { given: 2, expected: 0 },
+ bound: true,
+ imported: false,
+ locks: true
+ },
+ {
+ description: 'imported quote with a lock on step #2',
+ reason: '',
+ step: 2,
+ bound: false,
+ imported: true,
+ locks: false
+ }
+
+ ].forEach( testCase =>
+ {
+ const quote = BaseQuote( 123, {} );
+ const description = 'Locking is correct for ' + testCase.description;
+ const bound = !!testCase.bound;
+ const imported = !!testCase.imported;
+ const locks = !!testCase.locks;
+
+ const givenReason = ( testCase.reason.given !== undefined ) ?
+ '' + testCase.reason.given :
+ '' + testCase.reason;
+ const expectedReason = ( testCase.reason.expected !== undefined ) ?
+ '' + testCase.reason.expected :
+ '' + testCase.reason;
+
+ const givenStep = ( testCase.step.given !== undefined ) ?
+ +testCase.step.given :
+ +testCase.step;
+ const expectedStep = ( testCase.step.expected !== undefined ) ?
+ +testCase.step.expected :
+ +testCase.step;
+
+ it( description, () =>
+ {
+ expect( quote.getExplicitLockReason() ).to.equal( '' );
+ expect( quote.getExplicitLockStep() ).to.equal( 0 );
+
+ quote.setBound( bound )
+ .setImported( imported )
+ .setExplicitLock( givenReason, givenStep );
+
+ expect( quote.getExplicitLockReason() ).to.equal( expectedReason );
+ expect( quote.getExplicitLockStep() ).to.equal( expectedStep );
+ expect( quote.isLocked() ).to.equal( locks );
+
+ quote.clearExplicitLock();
+ expect( quote.getExplicitLockReason() ).to.equal( bound ? messages.bound : '' );
+ expect( quote.getExplicitLockStep() ).to.equal( 0 );
+ } );
+ } );
+ } );
+
+ describe( 'quote expiration', () =>
+ {
+ [
+ {
+ description: 'default values',
+ currentDate: 0,
+ expired: false
+ },
+ {
+ description: 'quote immediately after start',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ expirationDate: 7862400000,
+ currentDate: 86400000,
+ expired: false
+ },
+ {
+ description: 'quote immediately after rate',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ initialRatedDate: 172800000,
+ expirationDate: 2764800000,
+ currentDate: 172800000,
+ expired: false
+ },
+ {
+ description: 'quote 31 days after rate',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400,
+ initialRatedDate: 172800000,
+ expirationDate: 2764800000,
+ currentDate: 2851200000,
+ expired: true
+ },
+ {
+ description: 'quote 31 days after rate (with grace period)',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ preRateGracePeriod: 15,
+ postRateExpiration: 30,
+ postRateGracePeriod: 5
+ },
+ startDate: 86400,
+ initialRatedDate: 172800000,
+ expirationDate: 2764800000,
+ currentDate: 2851200000,
+ expired: false
+ },
+ {
+ description: 'quote 62 days after start',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ expirationDate: 7862400000,
+ currentDate: 5356800000,
+ expired: false
+ },
+ {
+ description: 'quote 61 days after rate',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ initialRatedDate: 172800000,
+ expirationDate: 2764800000,
+ currentDate: 5356800000,
+ expired: true
+ },
+ {
+ description: 'quote 91 days after start',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ expirationDate: 7862400000,
+ currentDate: 7948800000,
+ expired: true
+ },
+ {
+ description: 'quote 91 days after start (with grace period)',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ preRateGracePeriod: 15,
+ postRateExpiration: 30,
+ postRateGracePeriod: 5
+ },
+ startDate: 86400000,
+ expirationDate: 7862400000,
+ currentDate: 7948800000,
+ expired: false
+ },
+ {
+ description: 'quote 121 days after start',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ expirationDate: 7862400000,
+ currentDate: 10540800000,
+ expired: true
+ },
+ {
+ description: 'quote 120 days after rate',
+ lockTimeout:
+ {
+ preRateExpiration: 90,
+ postRateExpiration: 30
+ },
+ startDate: 86400000,
+ initialRatedDate: 172800000,
+ expirationDate: 2764800000,
+ currentDate: 10540800000,
+ expired: true
+ }
+
+ ].forEach( testCase =>
+ {
+ const quote = BaseQuote( 123, {} );
+ const description = "Expiration is correct for " + testCase.description;
+ const start_date = testCase.startDate;
+ const initial_rated_date = testCase.initialRatedDate;
+ const current_date = testCase.currentDate;
+ const expiration_date = testCase.expirationDate;
+ const expired = testCase.expired;
+
+ quote.setProgram( createStubProgram( testCase.lockTimeout ) );
+
+ it( description, () =>
+ {
+ if ( start_date !== undefined )
+ {
+ quote.setStartDate( start_date );
+ }
+
+ if ( initial_rated_date !== undefined )
+ {
+ quote.setInitialRatedDate( initial_rated_date );
+ }
+
+ if ( expiration_date !== undefined )
+ {
+ expect( quote.getExpirationDate() ).to.equal( +expiration_date );
+ }
+
+ expect( quote.hasExpired( new Date( current_date ) ) ).to.equal( !!expired );
+ } );
} );
} );
} );
+
+function createStubProgram( lockTimeout )
+{
+ let stubProgram = Util.stubProgram( Program )();
+ stubProgram.lockTimeout = lockTimeout || {};
+ return stubProgram;
+}
\ No newline at end of file
diff --git a/test/server/quote/ServerSideQuoteTest.js b/test/server/quote/ServerSideQuoteTest.js
new file mode 100644
index 0000000..c496e8d
--- /dev/null
+++ b/test/server/quote/ServerSideQuoteTest.js
@@ -0,0 +1,125 @@
+/**
+ * Tests ServerSideQuote
+ *
+ * Copyright (C) 2017, 2018 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.quote.ServerSideQuote;
+
+describe( 'ServerSideQuote', () =>
+{
+ describe( 'accessors & mutators', () =>
+ {
+ [
+ {
+ property: 'startDate',
+ default: 0,
+ value: 946684800
+ },
+ {
+ property: 'initialRatedDate',
+ default: 0,
+ value: 946684800
+ },
+ {
+ property: 'agentId',
+ default: 0,
+ value: 12345678
+ },
+ {
+ property: 'agentEntityId',
+ default: 0,
+ value: 12345678
+ },
+ {
+ property: 'agentName',
+ default: '',
+ value: 'name'
+ },
+ {
+ property: 'imported',
+ default: false,
+ value: true,
+ accessor: 'is'
+ },
+ {
+ property: 'bound',
+ default: false,
+ value: true,
+ accessor: 'is'
+ },
+ {
+ property: 'currentStepId',
+ default: 1,
+ value: 2
+ },
+ {
+ property: 'topVisitedStepId',
+ default: 1,
+ value: 2
+ },
+ {
+ property: 'topSavedStepId',
+ value: 1
+ },
+ {
+ property: 'error',
+ default: '',
+ value: 'ERROR'
+ },
+ {
+ property: 'programVersion',
+ default: '',
+ value: '1.0.0'
+ },
+ {
+ property: 'creditScoreRef',
+ default: 0,
+ value: 800
+ },
+ {
+ property: 'lastPremiumDate',
+ default: 0,
+ value: 946684800
+ },
+ {
+ property: 'ratedDate',
+ default: 0,
+ value: 946684800
+ }
+
+ ].forEach( testCase =>
+ {
+ const quote = Sut( 123, {} );
+ const property = testCase.property;
+ const title_cased = property.charAt( 0 ).toUpperCase() + property.slice( 1 );
+ const setter = ( testCase.mutator || 'set' ) + title_cased;
+ const getter = ( testCase.accessor || 'get' ) + title_cased;
+
+ it( property + ' can be mutated and accessed', () =>
+ {
+ expect( quote[getter].call( quote ) ).to.equal( testCase.default );
+ quote[setter].call( quote, testCase.value );
+ expect( quote[getter].call( quote ) ).to.equal( testCase.value );
+ } );
+ } );
+ } );
+} );
\ No newline at end of file