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" ],
+ },
+ };
+}
+
+
+