@codetanzania/majifix-jurisdiction
Version:
A representation of an entity (e.g municipal, local government etc) responsible for addressing citizen(or customer) service request(issue)
790 lines (739 loc) • 22.3 kB
JavaScript
import _ from 'lodash';
import { waterfall } from 'async';
import { idOf, randomColor, compact, mergeObjects } from '@lykmapipo/common';
import { createSchema, model, ObjectId } from '@lykmapipo/mongoose-common';
import actions from 'mongoose-rest-actions';
import exportable from '@lykmapipo/mongoose-exportable';
import {
Point,
MultiPolygon,
centroidOf,
TYPE_MULTIPOLYGON,
} from 'mongoose-geojson-schemas';
import { DEFAULT_COUNTRY_NAME, DEFAULT_CITY_NAME } from '@lykmapipo/constants';
import {
POPULATION_MAX_DEPTH,
MODEL_NAME_JURISDICTION,
MODEL_NAME_PRIORITY,
MODEL_NAME_STATUS,
MODEL_NAME_SERVICEGROUP,
MODEL_NAME_SERVICE,
MODEL_NAME_SERVICEREQUEST,
MODEL_NAME_ACCOUNT,
MODEL_NAME_CONTENT,
COLLECTION_NAME_JURISDICTION,
PATH_NAME_JURISDICTION,
checkDependenciesFor,
} from '@codetanzania/majifix-common';
/* constants */
const OPTION_SELECT = { code: 1, name: 1, color: 1, city: 1, country: 1 };
const OPTION_AUTOPOPULATE = {
select: OPTION_SELECT,
maxDepth: POPULATION_MAX_DEPTH,
};
const SCHEMA_OPTIONS = { collection: COLLECTION_NAME_JURISDICTION };
const INDEX_UNIQUE = { jurisdiction: 1, code: 1, name: 1 };
/**
* @module Jurisdiction
* @name Jurisdiction
* @description An entity (e.g municipal) responsible for addressing
* service request(issue).
*
* It may be a self managed entity or division within another
* entity(jurisdiction) in case there is hierarchy.
*
* @author lally elias <lallyelias87@gmail.com>
* @author Benson Maruchu <benmaruchu@gmail.com>
* @author Richard Aggrey <richardaggrey7@gmail.com>
* @license MIT
* @since 0.1.0
* @version 1.0.0
* @public
*/
const JurisdictionSchema = createSchema(
{
/**
* @name jurisdiction
* @description Top jurisdiction under which this jurisdiction derived.
*
* This is applicable where a large jurisdiction delegates
* its power to its division(s).
*
* If not set the jurisdiction will be treated as a top
* jurisdiction and will be affected by any logics implemented
* accordingly.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {string} ref - referenced collection
* @property {boolean} autoset - allow to set id from full object
* @property {boolean} exists - ensure ref exists before save
* @property {object} autopopulate - jurisdiction population options
* @property {object} autopopulate.select - jurisdiction fields to
* select when populating
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
jurisdiction: {
type: ObjectId,
ref: MODEL_NAME_JURISDICTION,
exists: { refresh: true, select: OPTION_SELECT },
autopopulate: OPTION_AUTOPOPULATE,
index: true,
},
/**
* @name code
* @description Unique human readable coded name of the jurisdiction.
*
* Used in deriving service request code.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} uppercase - force upper-casing
* @property {boolean} taggable - allow field use for tagging
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
code: {
type: String,
trim: true,
required: true,
uppercase: true,
taggable: true,
exportable: true,
searchable: true,
fake: {
generator: 'finance',
type: 'account',
unique: true,
},
index: true,
},
/**
* @name name
* @description Unique human readable name of the jurisdiction
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} taggable - allow field use for tagging
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
name: {
type: String,
trim: true,
required: true,
taggable: true,
exportable: true,
searchable: true,
fake: {
generator: 'address',
type: 'county',
},
index: true,
},
/**
* @name phone
* @description Primary mobile phone number used to contact jurisdiction.
*
* Used when a party want to send an SMS or call the jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} taggable - allow field use for tagging
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
phone: {
type: String,
trim: true,
required: true,
taggable: true,
exportable: true,
searchable: true,
fake: {
generator: 'phone',
type: 'phoneNumber',
},
index: true,
},
/**
* @name email
* @description Primary email address used to contact jurisdiction direct.
*
* Used when a party want to send direct mail to specific jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} lowercase - force lower-casing
* @property {boolean} taggable - allow field use for tagging
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
email: {
type: String,
trim: true,
required: true,
lowercase: true,
taggable: true,
exportable: true,
searchable: true,
fake: {
generator: 'internet',
type: 'email',
},
index: true,
},
/**
* @name website
* @description Primary website url of the jurisdiction.
*
* Used when a party want to obtain specific information about jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} lowercase - force lower-casing
* @property {boolean} taggable - allow field use for tagging
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
website: {
type: String,
trim: true,
lowercase: true,
taggable: true,
exportable: true,
searchable: true,
fake: {
generator: 'internet',
type: 'domainName',
},
index: true,
},
/**
* @name about
* @description A brief summary about jurisdiction if available i.e
* additional details that clarify what a jurisdiction do.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
about: {
type: String,
trim: true,
exportable: true,
searchable: true,
fake: {
generator: 'lorem',
type: 'paragraph',
},
index: true,
},
/**
* @name color
* @description A color code(in hexadecimal format) eg. #363636 used to
* differentiate jurisdictions visually.
*
* If not provided it will randomly generated, but it is not
* guarantee its visual appeal.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} uppercase - force upper-casing
* @property {boolean} exportable - allow field to be exported
* @property {boolean} default - default value
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
color: {
type: String,
trim: true,
uppercase: true,
exportable: true,
default: () => randomColor(),
fake: true,
},
/**
* @name country
* @description Human readable country name of jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {boolean} default - default value
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 1.8.0
* @version 1.0.0
* @instance
*/
country: {
type: String,
trim: true,
exportable: true,
searchable: true,
default: DEFAULT_COUNTRY_NAME,
fake: {
generator: 'address',
type: 'country',
},
index: true,
},
/**
* @name city
* @description Human readable city name of jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {boolean} default - default value
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 1.8.0
* @version 1.0.0
* @instance
*/
city: {
type: String,
trim: true,
exportable: true,
searchable: true,
default: DEFAULT_CITY_NAME,
fake: {
generator: 'address',
type: 'city',
},
index: true,
},
/**
* @name address
* @description Human readable physical address of jurisdiction office.
*
* Used when a party what to physical go or visit the jurisdiction office.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} exportable - allow field to be exported
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
* @property {boolean} index - ensure database index
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
address: {
type: String,
trim: true,
exportable: true,
searchable: true,
fake: {
generator: 'address',
type: 'streetAddress',
},
index: true,
},
/**
* @name location
* @description A center of jurisdiction. Its an office reachable
* by citizen(or customer)
*
* @type {object}
* @property {object} location - geo json point
* @property {string} location.type - Point
* @property {number[]} location.coordinates - longitude, latitude pair of
* the geo point
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* {
* type: 'Point',
* coordinates: [-76.80207859497996, 55.69469494228919]
* }
*/
location: Point,
/**
* @name boundaries
* @description Jurisdiction boundaries.
*
* Its mainly used for geo lookup of service request
* jurisdiction based on its geo coordinates.
*
* @type {object}
* @property {object} boundaries - geo json multi polygon
* @property {string} boundaries.type - MultiPolygon
* @property {number[]} boundaries.coordinates - collection of polygon
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* {
* type: 'MultiPolygon',
* coordinates: [ [ [ [-76.80207859497996, 55.69469494228919] ] ] ]
* }
*/
boundaries: MultiPolygon,
/**
* @name default
* @description Tells whether a jurisdiction is the default.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} index - ensure database index
* @property {boolean} exportable - allow field to be exported
* @property {boolean} default - default value set when none provided
* @property {object|boolean} fake - fake data generator options
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 0.1.0
* @instance
* @example
* false
*
*/
default: {
type: Boolean,
index: true,
exportable: true,
default: false,
fake: true,
},
},
SCHEMA_OPTIONS,
actions,
exportable
);
/*
*------------------------------------------------------------------------------
* Indexes
*------------------------------------------------------------------------------
*/
/**
* @name index
* @description ensure unique compound index on jurisdiction code and name
* to force unique jurisdiction definition
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 0.1.0
* @private
*/
JurisdictionSchema.index(INDEX_UNIQUE, { unique: true });
/*
*------------------------------------------------------------------------------
* Hooks
*------------------------------------------------------------------------------
*/
/**
* @name validate
* @description jurisdiction schema pre validation hook
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} valid instance or error
* @since 0.1.0
* @version 1.0.0
* @private
*/
JurisdictionSchema.pre('validate', function preValidate(done) {
return this.preValidate(done);
});
/*
*------------------------------------------------------------------------------
* Instance
*------------------------------------------------------------------------------
*/
/**
* @name preValidate
* @description jurisdiction schema pre validation hook logic
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} valid instance or error
* @since 0.1.0
* @version 1.0.0
* @instance
*/
JurisdictionSchema.methods.preValidate = function preValidate(done) {
// try compute unknown fields
try {
// set default color if not set
if (_.isEmpty(this.color)) {
this.color = randomColor();
}
// set jurisdiction code
if (_.isEmpty(this.code) && !_.isEmpty(this.name)) {
this.code = _.take(this.name, 1)
.join('')
.toUpperCase();
}
// ensure location
this.ensureLocation();
// continue
return done(null, this);
} catch (error) {
// catch and report errors
return done(error);
}
};
/**
* @name ensureLocation
* @description compute account location
* @returns {object} valid geojson point
* @since 0.1.0
* @version 1.0.0
* @instance
*/
JurisdictionSchema.methods.ensureLocation = function ensureLocation() {
// calculate boundaries centroid and set location if not available
if (this.boundaries) {
const centroid = centroidOf(this.boundaries);
this.location = centroid;
}
return this.location;
};
/**
* @name beforePost
* @description pre save account logics
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} valid instance or error
* @since 0.1.0
* @version 1.0.0
* @instance
*/
JurisdictionSchema.methods.beforePost = function beforePost(done) {
// perform pre save logics
try {
// ensure location
this.ensureLocation();
return done(null, this);
} catch (error) {
// catch and report error
return done(error);
}
};
/**
* @name beforeDelete
* @description pre delete jurisdiction logics
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} dependence free instance or error
* @since 0.1.0
* @version 1.0.0
* @instance
*/
JurisdictionSchema.methods.beforeDelete = function beforeDelete(done) {
// collect dependencies model name
const dependencies = [
MODEL_NAME_JURISDICTION,
MODEL_NAME_PRIORITY,
MODEL_NAME_STATUS,
MODEL_NAME_SERVICEGROUP,
MODEL_NAME_SERVICE,
MODEL_NAME_SERVICEREQUEST,
MODEL_NAME_ACCOUNT,
MODEL_NAME_CONTENT,
];
// path to check
const path = PATH_NAME_JURISDICTION;
// do check dependencies
return checkDependenciesFor(this, { path, dependencies }, done);
};
/*
*------------------------------------------------------------------------------
* Statics
*------------------------------------------------------------------------------
*/
/* static constants */
JurisdictionSchema.statics.MODEL_NAME = MODEL_NAME_JURISDICTION;
JurisdictionSchema.statics.OPTION_SELECT = OPTION_SELECT;
JurisdictionSchema.statics.OPTION_AUTOPOPULATE = OPTION_AUTOPOPULATE;
/**
* @name findDefault
* @function findDefault
* @description find default jurisdiction
* @param {Function} done a callback to invoke on success or failure
* @returns {Jurisdiction} default jurisdiction
* @since 0.1.0
* @version 1.0.0
* @static
*/
JurisdictionSchema.statics.findDefault = done => {
// refs
const Jurisdiction = model(MODEL_NAME_JURISDICTION);
// obtain default jurisdiction
return Jurisdiction.getOneOrDefault({}, done);
};
/**
* @name prepareSeedCriteria
* @function prepareSeedCriteria
* @description define seed data criteria
* @param {object} seed jurisdiction to be seeded
* @returns {object} packed criteria for seeding
*
* @author lally elias <lallyelias87@gmail.com>
* @since 1.6.0
* @version 0.1.0
* @static
*/
JurisdictionSchema.statics.prepareSeedCriteria = seed => {
const criteria = idOf(seed)
? _.pick(seed, '_id')
: _.pick(seed, ..._.keys(INDEX_UNIQUE));
return criteria;
};
/**
* @name getOneOrDefault
* @function getOneOrDefault
* @description Find existing jurisdiction or default based on given criteria
* @param {object} criteria valid query criteria
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} found jurisdiction or error
*
* @author lally elias <lallyelias87@gmail.com>
* @since 1.5.0
* @version 0.1.0
* @static
* @example
*
* const criteria = { _id: '...'};
* Jurisdiction.getOneOrDefault(criteria, (error, found) => { ... });
*
*/
JurisdictionSchema.statics.getOneOrDefault = (criteria, done) => {
// normalize criteria
const { _id, ...filters } = mergeObjects(criteria);
const allowDefault = true;
const allowId = !_.isEmpty(_id);
const allowFilters = !_.isEmpty(filters);
const byDefault = mergeObjects({ default: true });
const byId = mergeObjects({ _id });
const byFilters = mergeObjects(filters);
const or = compact([
allowId ? byId : undefined,
allowFilters ? byFilters : undefined,
allowDefault ? byDefault : undefined,
]);
const filter = { $or: or };
// refs
const Jurisdiction = model(MODEL_NAME_JURISDICTION);
// query
return Jurisdiction.findOne(filter)
.orFail()
.exec(done);
};
/**
* @name findNearBy
* @description find jurisdiction near a specified coordinates
* @param {object} options valid criteria
* @param {number} options.minDistance min distance in meters
* @param {number} options.maxDistance max distance in meters
* @param {number[]} options.coordinates coordinates of the location
* @param {Function} done a callback to invoke on success or error
* @returns {object[]} collection of jurisdiction near by specified coordinates
* @since 0.1.0
* @version 1.0.0
* @public
* @static
*/
JurisdictionSchema.statics.findNearBy = function findNearBy(options, done) {
// ref
const Jurisdiction = model(MODEL_NAME_JURISDICTION);
// default criteria
const criteria = {
$nearSphere: {
$geometry: {
type: TYPE_MULTIPOLYGON,
coordinates: [],
},
},
};
// set $geometry coordinates
if (_.isArray(options)) {
criteria.$nearSphere.$geometry.coordinates = _.compact(
criteria.$nearSphere.$geometry.coordinates.concat(options)
);
}
if (_.isPlainObject(options)) {
// set minDistance criteria
if (options.minDistance) {
criteria.$nearSphere.$minDistance = options.minDistance;
}
// set maxDistance criteria
if (options.maxDistance) {
criteria.$nearSphere.$maxDistance = options.maxDistance;
}
// ensure coordinates
criteria.$nearSphere.$geometry.coordinates = _.compact(
criteria.$nearSphere.$geometry.coordinates.concat(options.coordinates)
);
}
// find jurisdiction(s) which is near by provided coordinates
const ensureIndexes = next => Jurisdiction.ensureIndexes(() => next());
const queryNearBy = next => Jurisdiction.find({ boundaries: criteria }, next);
return waterfall([ensureIndexes, queryNearBy], done);
};
/* export jurisdiction model */
export default model(MODEL_NAME_JURISDICTION, JurisdictionSchema);