diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js new file mode 100644 index 0000000..dedb49e --- /dev/null +++ b/src/document/DocumentProgramFormatter.js @@ -0,0 +1,230 @@ +/** + * Formats program bucket data + * + * Copyright (C) 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 Class = require( 'easejs' ).Class; + + +/** + * Formats program bucket data + * + * This takes a document and formats the data in a + * structured manner. + */ +module.exports = Class( 'DocumentProgramFormatter', +{ + /** + * Current program + * + * @type {Program} + */ + 'private _program': null, + + /** + * Performs classification matching on fields + * + * A field will have a positive match for a given index if all of its + * classes match + * + * @type {FieldClassMatcher} + */ + 'private _class_matcher': null, + + + /** + * Initialize document formatter + * + * @param {Program} program active program + * @param {FieldClassMatcher} class_matcher class/field matcher + */ + constructor( program, class_matcher ) + { + this._program = program; + this._class_matcher = class_matcher; + }, + + + /** + * Returns formatted document bucket data + * Calls FieldClassMatcher.match to retrieve + * index of show/hide values for each field + * + * @param {Bucket} bucket document bucket + * + * @return {Object} JSON object + */ + 'public format'( bucket ) + { + return new Promise( ( resolve, reject ) => + { + const cmatch = this._program.classify( bucket.getData() ); + + this._class_matcher.match( cmatch, ( field_matches ) => + { + const len = this._program.steps.length; + const data = this._parseSteps( len, bucket, field_matches ); + + resolve( data ); + } ); + + } ); + }, + + + /** + * Parses step data + * + * @param {Integer} len step length + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Object} step data + */ + 'private _parseSteps'( len, bucket, field_matches ) + { + const data = { steps: [] }; + + for ( let i = 1; i < len; i++ ) + { + const step = {}; + const step_groups = this._program.steps[ i ].groups; + + const groups = this._parseGroups( step_groups, bucket, field_matches ); + + step.title = this._program.steps[ i ].title; + step.groups = groups; + + data.steps.push( step ); + } + + return data; + }, + + + /** + * Parses group data + * + * @param {Array} step_groups array of group data + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Array} array of groups + */ + 'private _parseGroups'( step_groups, bucket, field_matches ) + { + const groups = []; + + for ( let group in step_groups ) + { + const step_group = {}; + const group_id = step_groups[ group ]; + const group_title = this._program.groups[ group_id ].title || ""; + const fields = this._program.groupExclusiveFields[ group_id ]; + + const questions = this._parseFields( fields, bucket, field_matches ); + + step_group.title = group_title; + step_group.questions = questions; + groups.push( step_group ); + } + + return groups; + }, + + + /** + * Parses fields/question data + * + * @param {Array} fields array of field data + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Array} array of questions + */ + 'private _parseFields'( fields, bucket, field_matches ) + { + const questions = []; + + const classes = field_matches.__classes; + + for ( let field in fields ) + { + const field_id = fields[ field ]; + + // Don't include fields that are not in program.fields + if ( typeof this._program.fields[ field_id ] === "undefined" ) + { + continue; + } + + const field_value = bucket.getDataByName( field_id ); + const field_label = this._program.fields[ field_id ].label; + const question = {}; + + question.id = field_id; + question.label = field_label; + question.value = field_value; + question.applicable = this._getApplicable( + classes, + field_id, + field_value + ); + + questions.push( question ); + } + + return questions; + }, + + + /** + * Determine when a field is shown by index + * Map boolean values of [0, 1] to [true, false] + * + * @param {Object} classes object of visibility classes + * @param {String} field_id id of field + * @param {Object} field_value field object + * + * @return {Array.} array of booleans + */ + 'private _getApplicable'( classes, field_id, field_value ) + { + // If object is undefined, default to array of true + if ( typeof this._program.whens[ field_id ] === "undefined" ) + { + return field_value.map( _ => true ); + } + + const class_id = this._program.whens[ field_id ][ 0 ]; + const indexes = classes[ class_id ].indexes; + + // Map indexes of 0, 1 to true, false + if ( Array.isArray( indexes ) ) + { + return indexes.map( x => !!x ); + } + else + { + return field_value.map( _ => !!indexes ); + } + }, +} ); diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 17fda41..f494162 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -53,6 +53,14 @@ const { DataApiManager, }, + document: { + DocumentProgramFormatter, + }, + + field: { + FieldClassMatcher, + }, + server: { DocumentServer, @@ -450,6 +458,22 @@ function doRoute( program, request, data, resolve, reject ) } ); } ); } + else if ( cmd == 'progdata' ) + { + acquireReadLock( quote_id, request, function() + { + handleRequest( function( quote ) + { + const response = UserResponse( request ); + const bucket = quote.getBucket(); + const class_matcher = FieldClassMatcher( program.whens ); + + DocumentProgramFormatter( program, class_matcher ).format( bucket ) + .then( data => response.ok( data ) ) + .catch( e => response.error( e ) ); + } ); + } ); + } else if ( cmd === 'mkrev' ) { // the database operation for this is atomic and disjoint from diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js new file mode 100644 index 0000000..630e894 --- /dev/null +++ b/test/document/DocumentProgramFormatterTest.js @@ -0,0 +1,247 @@ +/** + * Test of DocumentProgramFormatter + * + * Copyright (C) 2018 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 chai = require( 'chai' ); +const expect = chai.expect; +const sinon = require( 'sinon' ); + +const { + document: { + DocumentProgramFormatter: Sut, + }, + + field: { + FieldClassMatcher, + }, + +} = require( '../../' ); + +chai.use( require( 'chai-as-promised' ) ); + + +describe( 'DocumentProgramFormatter', () => +{ + it( "formats bucket data", () => + { + const bucket_data = { + sell_alcohol: [ "foo", "" ], + serve_alcohol: [ "" ], + sell_ecigs: [ "", "bar" ], + dist_ecigs: [ "" ], + field_no_label: [ "" ], + field_no_array: [ "bar" ], + field_no_vis: [ "true" ] + }; + + const expected_object = { + steps: [ + { + title: "Manage Quote", + groups: [] + }, + { + title: "General Information", + groups: [ + { + title: "Group One", + questions: [ + { + id: "sell_alcohol", + label: "Does the insured sell alcohol?", + value: [ "foo", "" ], + applicable: [ true, false ] + }, + { + id: "serve_alcohol", + label: "Does the insured serve alcohol?", + value: [ "" ], + applicable: [ false ] + }, + { + id: "field_no_vis", + label: "Does this field have a visibility class?", + value: [ "true" ], + applicable: [ true ] + } + ] + }, + { + title: "", + questions: [ + { + id: "sell_ecigs", + label: "Does the insured sell e-cigarettes?", + value: [ "", "bar" ], + applicable: [ false, true ] + }, + { + id: "dist_ecigs", + label: "Does the Insured distribute Electronic Cigarette products?", + value: [ "" ], + applicable: [ false ] + }, + { + id: "field_no_array", + label: "Does this field have an array for the visibility class?", + value: [ "bar" ], + applicable: [ true ] + } + ] + } + ] + } + ] + }; + + const bucket = createStubBucket( bucket_data ); + const program = createStubProgram(); + const class_matcher = createStubClassMatcher(); + + return expect( + Sut( program, class_matcher ).format( bucket ) + ).to.eventually.deep.equal( expected_object ); + } ); + +} ); + + +function createStubClassMatcher() +{ + return { + match( _, callback ) + { + callback({ + __classes: + { + '--vis-sell-alcohol': { is: true, indexes: [1,0] }, + '--vis-serve-alcohol': { is: false, indexes: [0] }, + '--vis-sell-ecigs': { is: false, indexes: [0,1] }, + '--vis-dist-ecigs': { is: true, indexes: [0] }, + '--vis-no-array': { is: true, indexes: 1 }, + } + }) ; + } + } +} + + +function createStubBucket( metadata ) +{ + return { + getDataByName: name => metadata[ name ], + getData() + { + return metadata; + }, + }; +} + + +function createStubProgram() +{ + return { + steps: [ + { + title: "Index 0", + groups: [] + }, + { + title: "Manage Quote", + groups: [] + }, + { + title: "General Information", + groups: [ 'group_one', 'group_two' ] + } + ], + classify( bucket_data ) + { + return {} + }, + groups: + { + 'group_one': + { + title: "Group One" + }, + 'group_two': {}, + }, + fields: + { + sell_alcohol: + { + label: "Does the insured sell alcohol?", + type: "noyes", + required: "true", + }, + serve_alcohol: + { + label: "Does the insured serve alcohol?", + type: "noyes", + required: "true" + }, + sell_ecigs: + { + label: "Does the insured sell e-cigarettes?", + type: "noyes", + required: "true" + }, + dist_ecigs: + { + label: "Does the Insured distribute Electronic Cigarette products?", + type: "noyes", + required: "true" + }, + field_no_array: + { + label: "Does this field have an array for the visibility class?", + type: "noyes", + required: "true" + }, + field_no_vis: + { + label: "Does this field have a visibility class?", + type: "noyes", + required: "true" + } + }, + groupExclusiveFields: + { + 'group_one': [ "sell_alcohol", "serve_alcohol", "field_no_label", "field_no_vis" ], + 'group_two': [ "sell_ecigs", "dist_ecigs", "field_no_array" ], + + }, + whens: + { + sell_alcohol: [ "--vis-sell-alcohol" ], + serve_alcohol: [ "--vis-serve-alcohol" ], + sell_ecigs: [ "--vis-sell-ecigs" ], + dist_ecigs: [ "--vis-dist-ecigs" ], + field_no_array: [ "--vis-no-array" ], + }, + }; +} + + +