@codetanzania/majifix-account
Version:
A representation of an entity (i.e organization, individual, customer, or client) which receiving service(s) from a particular jurisdiction
2,049 lines (1,902 loc) • 58.5 kB
JavaScript
import _ from 'lodash';
import { pkg } from '@lykmapipo/common';
import { getString, getStringSet, apiVersion } from '@lykmapipo/env';
import { Router, start } from '@lykmapipo/express-common';
import async from 'async';
import moment from 'moment';
import actions from 'mongoose-rest-actions';
import exportable from '@lykmapipo/mongoose-exportable';
import { Point } from 'mongoose-geojson-schemas';
import { toE164 } from '@lykmapipo/phone';
import { createSubSchema, createSchema, ObjectId, Mixed, model } from '@lykmapipo/mongoose-common';
import { MODEL_NAME_ACCOUNT, POPULATION_MAX_DEPTH, COLLECTION_NAME_ACCOUNT } from '@codetanzania/majifix-common';
import { Jurisdiction } from '@codetanzania/majifix-jurisdiction';
/**
* @name Period
* @description a period under which a bill is applicable.
* Its is the period of time between billings.
*
* @see {@link https://www.thebalance.com/billing-cycle-960690}
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const Period = createSubSchema({
/**
* @name name
* @description Human readable period name e.g November, Jan-Jun etc
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* January, Jan-Jun
*/
name: {
type: String,
trim: true,
},
/**
* @name billedAt
* @description A date when a bill come to effect
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 2018-01-01
*/
billedAt: {
type: Date,
fake: {
generator: 'date',
type: 'past',
},
},
/**
* @name startedAt
* @description A bill period start date(or time)
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 2018-01-01
*/
startedAt: {
type: Date,
fake: {
generator: 'date',
type: 'past',
},
},
/**
* @name endedAt
* @description A bill period end date(or time)
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 2018-01-20
*/
endedAt: {
type: Date,
fake: {
generator: 'date',
type: 'recent',
},
},
/**
* @name duedAt
* @description A bill period due date(or time). Mostly used by
* jurisdiction to refer the date when an account should have
* already pay the bill.
*
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 2018-01-30
*/
duedAt: {
type: Date,
fake: {
generator: 'date',
type: 'future',
},
},
});
/**
* @name Balance
* @description represents how much is owed on a bill(or invoice).
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const Balance = createSubSchema({
/**
* @name outstand
* @description Current bill period outstand balance
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 800
*/
outstand: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name open
* @description Current bill period open balance
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 200
*/
open: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name charges
* @description Current bill period charges
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 100
*/
charges: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name debt
* @description Current bill period account total additional
* debt e.g loan etc.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 700
*/
debt: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name close
* @description Current bill period close balance
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 300
*/
close: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
});
/**
* @name SubItem
* @description bill(or invoice) sub-item(or deriving item). Its a
* collection of item used to derive a bill item.
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const SubItem = createSubSchema({
/**
* @name name
* @description Human readable name of bill sub item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* Previous Reeding
*/
name: {
type: String,
trim: true,
fake: {
generator: 'commerce',
type: 'productName',
},
},
/**
* @name quantity
* @description Sub item quantity
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* 5
*/
quantity: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name price
* @description Sub item total price e.g if quantity if 5 then price
* must be total for all of the 5 item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* 6000
*/
price: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name unit
* @description Human readable unit of sub item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* Previous Reeding is in cubic meter so its cbm
*/
unit: {
type: String,
trim: true,
},
/**
* @name name
* @description Date when a sub item realized. e.g Date when a
* meter reading was taken.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* 2018-06-01
*/
time: {
type: Date,
fake: {
generator: 'date',
type: 'recent',
},
},
});
/**
* @name Item
* @description bill(or invoice) item(or line item)
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const Item = createSubSchema({
/**
* @name name
* @description Human readable name of bill item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* Water Consumption
*/
name: {
type: String,
trim: true,
fake: {
generator: 'commerce',
type: 'productName',
},
},
/**
* @name quantity
* @description Bill item quantity
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* 5
*/
quantity: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name price
* @description Bill item total price e.g if quantity if 5 then price
* must be total for all of the 5 item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* 6000
*/
price: {
type: Number,
fake: {
generator: 'random',
type: 'number',
},
},
/**
* @name unit
* @description Human readable unit of bill item.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* Water Consumption is in cubic meter so its cbm
*/
unit: {
type: String,
trim: true,
},
/**
* @name name
* @description Date when a bill item realized. e.g Date when a
* meter reading was taken.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
* @example
* 2018-06-01
*/
time: {
type: Date,
fake: {
generator: 'date',
type: 'recent',
},
},
/**
* @name items
* @description collection of items used to derive this item
*
* @type {object}
* @property {object} type - schema(data) type
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
items: [SubItem],
});
/**
* @name Bill
* @description account bill(or invoice) from jurisdiction.
*
* @see {@link https://github.com/CodeTanzania/majifix-jurisdiction}
* @see {@link https://en.wikipedia.org/wiki/Invoice}
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const Bill = createSubSchema({
/**
* @name number
* @description Unique human readable bill number(i.e invoice number,
* payment control number etc) issued by jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} uppercase - force upper casing
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* TP4547
*/
number: {
type: String,
trim: true,
uppercase: true,
},
/**
* @name period
* @description A bill period under which an account is obligated to
* cover for the service from jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* {
* form: 2018-01-01
* to: 2018-01-31
* }
*/
period: Period,
/**
* @name balance
* @description Current bill balances
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* {
* outstand: 1200,
* open: 800,
* charges: 200,
* close: 1200,
* debt: 200
* }
*/
balance: Balance,
/**
* @name items
* @description Current bill items
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* {
* name: 'Clean Water',
* quantity: 2,
* price: 400,
* unit: 'cbm'
* }
*/
items: [Item],
/**
* @name currency
* @description Human readable bill currency code(i.e USD, TZS etc).
*
* Mostly used when format bill amounts per specific locale.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} uppercase - force upper casing
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* TZS, USD etc
*/
currency: {
type: String,
trim: true,
uppercase: true,
fake: {
generator: 'finance',
type: 'currencyCode',
},
},
/**
* @name notes
* @description Additional human readable information about the
* bill from jurisdiction.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*
* @example
* You should pay this bill before its due date
*/
notes: {
type: String,
trim: true,
fake: {
generator: 'lorem',
type: 'sentence',
},
},
});
/* constants */
const DEFAULT_LOCALE = getString('DEFAULT_LOCALE', 'en');
const LOCALES = getStringSet('LOCALES', [DEFAULT_LOCALE]);
const SUB_SCHEMA_OPTIONS = { timestamps: true };
/**
* @name Accessor
* @description list of parties(individual etc) that are allowed to
* access account.
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const Accessor = createSubSchema(
{
/**
* @name name
* @description Name of the accessor
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
name: {
type: String,
trim: true,
index: true,
fake: {
generator: 'name',
type: 'findName',
},
},
/**
* @name phone
* @description Valid mobile phone number of the accessor
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
phone: {
type: String,
trim: true,
index: true,
fake: {
generator: 'phone',
type: 'phoneNumber',
},
},
/**
* @name email
* @description Valid email address of the accessor
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} lowercase - force lower-casing
* @property {boolean} index - ensure database index
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
email: {
type: String,
trim: true,
lowercase: true,
index: true,
fake: {
generator: 'internet',
type: 'email',
},
},
/**
* @name locale
* @description Defines the accessor language, region and any
* special variant preferences.
*
* Used to localize(format) account user interfaces.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} default - default value set when none provided
* @property {object} fake - fake data generator options
*
* @see {@link https://en.wikipedia.org/wiki/Locale_(computer_software)}
* @private
* @since 0.1.0
* @version 1.0.0
*/
locale: {
type: String,
trim: true,
index: true,
default: DEFAULT_LOCALE,
enum: LOCALES,
fake: true,
},
/**
* @name verifiedAt
* @description A date when accessor verified
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} index - ensure database index
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
verifiedAt: {
type: Date,
index: true,
fake: {
generator: 'date',
type: 'soon',
},
},
},
SUB_SCHEMA_OPTIONS
);
/* constants */
const DEFAULT_LOCALE$1 = getString('DEFAULT_LOCALE', 'en');
const OPTION_SELECT = {
number: 1,
identity: 1,
name: 1,
phone: 1,
email: 1,
locale: 1,
};
const OPTION_AUTOPOPULATE = {
select: OPTION_SELECT,
maxDepth: POPULATION_MAX_DEPTH,
};
const SCHEMA_OPTIONS = { collection: COLLECTION_NAME_ACCOUNT };
const INDEX_UNIQUE = { jurisdiction: 1, number: 1, identity: 1 };
/* helpers */
const isEmpty = v => {
if (_.isNumber(v)) {
return false;
}
if (_.isBoolean(v)) {
return false;
}
if (_.isDate(v)) {
return false;
}
return _.isEmpty(v);
};
/**
* @name Account
* @description A representation of an entity
* (i.e organization, individual, customer, or client) which
* receiving service(s) from a particular jurisdiction.
*
* @see {@link https://github.com/CodeTanzania/majifix-jurisdiction}
* @see {@link https://en.wikipedia.org/wiki/Customer}
* @author Benson Maruchu <benmaruchu@gmail.com>
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 1.0.0
* @public
*/
const AccountSchema = createSchema(
{
/**
* @name jurisdiction
* @description jurisdiction under which this account belongs.
*
* This is applicable where multiple jurisdiction(s) utilize
* same majifix system(or platform)
*
* @type {object}
* @property {object} type - schema(data) type
* @property {string} ref - referenced collection
* @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: Jurisdiction.MODEL_NAME,
exists: { refresh: true, select: Jurisdiction.OPTION_SELECT },
autopopulate: Jurisdiction.OPTION_AUTOPOPULATE,
index: true,
},
/**
* @name category
* @description Human readable category of the account(or customer)
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
category: {
type: String,
trim: true,
index: true,
searchable: true,
fake: {
generator: 'name',
type: 'jobType',
},
},
/**
* @name number
* @description Unique human readable account number.
*
* Used as a unique identifier for an account per jurisdiction.
*
* This should be a real account number from e.g billing system,
* membership database etc.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {boolean} uppercase - force upper casing
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
number: {
type: String,
trim: true,
required: true,
index: true,
searchable: true,
uppercase: true,
fake: {
generator: 'finance',
type: 'account',
},
},
/**
* @name identity
* @description Human readable account identifier.
*
* This may be e.g meter number, facility id etc.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {boolean} uppercase - force upper casing
* @property {object} fake - fake data generator options
*
* @since 1.3.0
* @version 1.0.0
* @instance
*/
identity: {
type: String,
trim: true,
index: true,
searchable: true,
uppercase: true,
fake: {
generator: 'finance',
type: 'account',
},
},
/**
* @name name
* @description Human readable name of the account
*
* This is either a full name of an individual or organization
* that a jurisdiction used to when refer to the account.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
name: {
type: String,
trim: true,
required: true,
index: true,
searchable: true,
fake: {
generator: 'name',
type: 'findName',
},
},
/**
* @name phone
* @description Primary mobile phone number used to contact an account
* direct by a jurisdiction.
*
* Used when a jurisdiction want to send an sms message or
* call the account.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
phone: {
type: String,
required: true,
trim: true,
index: true,
searchable: true,
fake: {
generator: 'phone',
type: 'phoneNumber',
},
},
/**
* @name email
* @description Primary email address used to contact an account direct
* by a jurisdiction.
*
* Used when a jurisdiction want to send direct mail to the
* account.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} required - mark required
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {boolean} lowercase - force lower casing
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
email: {
type: String,
trim: true,
index: true,
searchable: true,
lowercase: true,
fake: {
generator: 'internet',
type: 'email',
},
},
/**
* @name neighborhood
* @description Human readable district or town of an account.
*
* Used when a jurisdiction what to target accounts
* resides in a specific area within its boundaries.
*
* @see {@link https://en.wikipedia.org/wiki/Neighbourhood}
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
neighborhood: {
type: String,
trim: true,
index: true,
searchable: true,
fake: {
generator: 'address',
type: 'county',
},
},
/**
* @name address
* @description Human readable physical address of an account.
*
* Used when a jurisdiction what to physical go or visit the
* the account.
*
* @see {@link https://en.wikipedia.org/wiki/Address_(geography)}
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
address: {
type: String,
trim: true,
index: true,
searchable: true,
fake: {
generator: 'address',
type: 'streetAddress',
},
},
/**
* @name locale
* @description Defines the account's language, region and any
* special variant preferences.
*
* Used to localize(format) account user interfaces.
*
* @see {@link https://en.wikipedia.org/wiki/Locale_(computer_software)}
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} trim - force trimming
* @property {boolean} index - ensure database index
* @property {boolean} searchable - allow for searching
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
locale: {
type: String,
trim: true,
searchable: true,
index: true,
default: DEFAULT_LOCALE$1,
fake: true,
},
/**
* @name location
* @description jurisdiction point of interest on account.
*
* This may be a point where there an installed meter,
* antenna, house etc.
*
* @see {@link https://tools.ietf.org/html/rfc7946#page-22}
*
* @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 accessors
* @description List of individuals who can access account
* information
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
accessors: [Accessor],
/**
* @name bills
* @description Latest account bills of the account from the jurisdiction.
*
* This is optional as to some of jurisdiction(s) is not applicable.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
bills: [Bill],
/**
* @name extras
* @description account additional details
*
* @type {object}
*
* @since 0.1.0
* @version 0.1.0
* @instance
* @example
* {
* 'dob': '1988-06-25'
* }
*/
extras: {
type: Mixed,
set: function set(val) {
const value = _.merge({}, val);
return value;
},
fake: {
generator: 'helpers',
type: 'userCard',
},
},
/**
* @name fetchedAt
* @description Latest time when account is synchronized from it source i.e
* Billing System(or API) etc.
*
* @type {object}
* @property {object} type - schema(data) type
* @property {boolean} index - ensure database index
* @property {boolean} hide - hide field
* @property {boolean} default - value to set when non provided
* @property {object} fake - fake data generator options
*
* @since 0.1.0
* @version 1.0.0
* @instance
*/
fetchedAt: {
type: Date,
index: true,
hide: true,
default: moment(new Date())
.subtract(1, 'years')
.toDate(),
fake: true,
},
},
SCHEMA_OPTIONS,
actions,
exportable
);
/*
*------------------------------------------------------------------------------
* Indexes
*------------------------------------------------------------------------------
*/
/**
* @name index
* @description ensure unique compound index on number, identity
* and jurisdiction to force unique account definition
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.1.0
* @version 0.1.0
* @private
*/
AccountSchema.index(INDEX_UNIQUE, { unique: true });
/*
*------------------------------------------------------------------------------
* Hooks
*------------------------------------------------------------------------------
*/
/**
* @name preValidate
* @function preValidate
* @description schema pre validation hook
* @param {Function} done callback to invoke on success or error
* @since 0.1.0
* @version 0.1.0
* @private
*/
AccountSchema.pre('validate', function cb(next) {
// ensure location details
this.ensureLocation();
// ensure unique accessors
this.ensureUniqueAccessors();
// always sort bills in desc order
this.bills = _.orderBy(this.bills, 'period.billedAt', 'desc');
next();
});
/*
*------------------------------------------------------------------------------
* Instance
*------------------------------------------------------------------------------
*/
/**
* @name ensureUniqueAccessors
* @function ensureUniqueAccessors
* @description clear duplicate accessors
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.ensureUniqueAccessors = function ensureUniqueAccessors() {
// obtain accessors
const accessors = [].concat(this.accessors);
// remove duplicate accessors
const objects = {};
_.forEach(accessors, function cb(accessor) {
if (accessor && !_.isEmpty(accessor.phone)) {
let object = _.get(objects, accessor.phone);
object = _.merge({}, object, accessor.toObject());
if (object.phone) {
objects[object.phone] = object;
}
}
});
// reset accessors
this.accessors = _.values(objects);
// return self
return this;
};
/**
* @name upsertAccessor
* @function upsertAccessor
* @description update existing accessor
* @param {string} phone valid accessor phone number
* @param {object} updates valid accessor updates
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.upsertAccessor = function upsertAccessor(phone, updates) {
// obtain unique accessors
this.ensureUniqueAccessors();
let accessors = [].concat(this.accessors);
// obtain exist accessor
let accessor = _.find(accessors, function cb(accountAccessor) {
return accountAccessor.phone === phone;
});
// remove found accessor
accessors = _.filter(accessors, function cb(accountAccessor) {
return accountAccessor.phone !== phone;
});
// merge accessor details
accessor = accessor ? accessor.toObject() : {};
accessor = _.merge({}, accessor, { phone }, updates);
// ensure e.164 format
accessor.phone = toE164(accessor.phone);
// update accessors
accessors = [].concat(accessors).concat(accessor);
this.accessors = accessors;
this.ensureUniqueAccessors();
// return self
return this;
};
/**
* @name removeAccessor
* @function removeAccessor
* @description update existing accessor
* @param {object} phone valid accessor phone number
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.removeAccessor = function removeAccessor(phone) {
// obtain unique accessors
this.ensureUniqueAccessors();
let accessors = [].concat(this.accessors);
// remove required accessor
accessors = _.filter(accessors, function cb(accessor) {
return accessor.phone !== phone;
});
// update accessors
this.accessors = [].concat(accessors);
// return self
return this;
};
/**
* @name ensureLocation
* @function ensureLocation
* @description compute account location
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.ensureLocation = function ensureLocation() {
// check if account should be able to set location
const shouldSetLocation =
!this.location &&
this.jurisdiction &&
_.isFunction(this.jurisdiction.ensureLocation);
if (shouldSetLocation) {
// ensure jurisdiction location
this.jurisdiction.ensureLocation();
// ensure account location
if (this.jurisdiction.location) {
this.location = _.merge({}, this.jurisdiction.location.toObject());
}
}
return this.location;
};
/**
* @name beforePost
* @function beforePost
* @description pre save account logics
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} valid account instance or error
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.beforePost = function beforePost(done) {
// ensure location
this.ensureLocation();
// ensure unique accessors
this.ensureUniqueAccessors();
return done(null, this);
};
/**
* @name beforePatch
* @function beforePatch
* @description pre patch account logics
* @param {object} updates patches to be applied to account instance
* @param {Function} done callback to invoke on success or error
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.beforePatch = function beforePatch(updates, done) {
return this.beforePost(done);
};
/**
* @name beforePut
* @function beforePut
* @description pre put account logics
* @param {object} updates patches to be applied to account instance
* @param {Function} done callback to invoke on success or error
* @returns {object} valid account instance
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.beforePut = function beforePut(updates, done) {
return this.beforePost(done);
};
/**
* @name afterPost
* @function afterPost
* @description post save account logics
* @param {Function} done callback to invoke on success or error
* @since 0.1.0
* @version 1.0.0
* @instance
*/
AccountSchema.methods.afterPost = function afterPost(done) {
done();
};
/*
*------------------------------------------------------------------------------
* Statics
*------------------------------------------------------------------------------
*/
/* static constants */
AccountSchema.statics.MODEL_NAME = MODEL_NAME_ACCOUNT;
AccountSchema.statics.OPTION_SELECT = OPTION_SELECT;
AccountSchema.statics.OPTION_AUTOPOPULATE = OPTION_AUTOPOPULATE;
/**
* @name afterGet
* @function afterGet
* @description after get query logics
* @param {object} mquery valid mquery
* @param {object} result valid get results
* @param {Function} done callback to invoke on success or error
* @since 0.1.0
* @version 1.0.0
* @static
*/
AccountSchema.statics.afterGet = function afterGet(mquery, result, done) {
// ref
const Account = this;
const results = result;
// check for filter
const { filter } = _.merge({}, { filter: {} }, mquery);
const identity = _.toUpper(filter.number || filter.identity);
// not data try fetch from providers
const shouldFetch =
results && _.isEmpty(results.data) && !_.isEmpty(identity);
if (shouldFetch) {
Account.fetchAndUpsert(identity, function cb(error, account) {
if (!error && account) {
const data = [].concat(results.data).concat(account);
results.data = data;
results.total = results.size;
results.size = data.length;
}
// TODO handle swallowed error
done(null, results);
});
}
// continue with same results
else {
done(null, results);
}
};
/**
* @name verify
* @function verify
* @description verify if the requestor can access account
* @param {object} requestor valid requestor details
* @param {Function} done a callback to invoke on success or error
* @returns {Account|Error} valid account instance or error
* @since 0.1.0
* @version 1.0.0
* @static
*/
AccountSchema.statics.verify = function verify(requestor, done) {
/* @todo verify device */
// ensure accessor
const accessor = _.merge({}, { shallow: false }, requestor);
accessor.number = _.toUpper(accessor.number) || _.toUpper(accessor.account);
accessor.identity = _.toUpper(accessor.identity);
// refs
const Account = this;
// ensure phone number
if (_.isEmpty(accessor.phone)) {
const error = new Error('Missing Phone Number');
error.status = 400;
return done(error);
}
// ensure account number
if (_.isEmpty(accessor.number) && _.isEmpty(accessor.identity)) {
const error = new Error('Missing Account Number or Identity');
error.status = 400;
return done(error);
}
// start verify
return async.waterfall(
[
// 1...retrieve account by account number
function findAccountByIdentities(next) {
const { number, identity } = accessor;
Account.fetchAndUpsert(number || identity, function cb(error, account) {
let cbError = error;
if (!account) {
cbError = new Error('Invalid Account Number or Identity');
cbError.status = 400;
}
next(cbError, account);
});
},
// 2...verify phone number for access
function verifyPhoneNumberAccess(account, next) {
// check if accessor phone is allowed
let phones = _.chain([].concat(account.accessors)).map(function cb(
accountAccessor
) {
if (accountAccessor.verifiedAt) {
return accountAccessor.phone;
}
return undefined;
});
phones = phones
.union([account.phone])
.compact()
.uniq()
.value();
const isAllowed = _.includes(phones, accessor.phone);
// allow access
if (isAllowed) {
next(null, account);
}
// request access and notify forbidden
else {
// add accessor request
const guest = _.merge({}, accessor, {
phone: toE164(accessor.phone),
});
account.upsertAccessor(guest.phone, guest);
/* @todo create new service request */
/* @todo notify account owner */
/* @todo notify system admins */
// persist account
account.put(function cb(/* error, saved */) {
// notify request accepted
const error = new Error('Waiting for Verification');
error.status = 202;
// allow shallow access
if (accessor.shallow) {
next(null, account);
}
// restrict access
else {
next(error);
}
});
}
},
],
function cb(error, result) {
const cbError = error;
// ensure error status
if (cbError && !cbError.status) {
cbError.status = 400;
}
return done(cbError, result);
}
);
};
/**
* @name fetch
* @function fetch
* @description pull account from the provided source
* @param {string} identity valid account number or identity
* @param {Date} fetchedAt last fetch date of account from it source
* @param {Function} done a callback to invoke on success or error
* @returns {object|Error} valid account instance or error
* @since 0.1.0
* @version 1.0.0
* @static
*/
AccountSchema.statics.fetch = function fetch(identity, fetchedAt, done) {
// refs
const Account = this;
// ensure arguments
const hasArguments =
_.isString(identity) && _.isDate(fetchedAt) && _.isFunction(done);
// ensure callback
let cb = _.isFunction(identity) ? identity : done;
cb = _.isFunction(fetchedAt) ? fetchedAt : cb;
// check fetch provider
const canFetch =
_.isFunction(Account.fetchAccount) && Account.fetchAccount.length === 3;
// do fetch account from provider
if (hasArguments && canFetch) {
return Account.fetchAccount(identity, fetchedAt, function afterFetch(
error,
fetched
) {
let _fetched = fetched; // eslint-disable-line no-underscore-dangle
if (!error && !_.isEmpty(fetched)) {
_fetched = _.merge({}, { fetchedAt: new Date() }, fetched);
_fetched = _.omitBy(_fetched, isEmpty);
}
cb(error, _fetched);
});
}
// dont fetch: return
return cb(null, {});
};
/**
* @name fetchAndUpsert
* @function fetchAndUpsert
* @description pull account from the provided source and upsert
* @param {string} identity valid account number or identity
* @param {Function} done a callback to invoke on success or error
* @returns {object|Error} valid account instance or error
* @since 0.1.0
* @version 1.0.0
* @static
*/
AccountSchema.statics.fetchAndUpsert = function fetchAndUpsert(identity, done) {
// ref
const Account = this;
// prepare last fetched date
const fetchedAt = moment(new Date())
.subtract(1, 'years')
.toDate();
return async.waterfall(
[
// 1.
function findExisting(next) {
const criteria = {
$or: [{ number: identity }, { identity }],
};
Account.findOne(criteria, function cb(error, found) {
next(error, found || {});
});
},
// 2.
function fetchAccount(account, next) {
// prepare latest fetched date
let _fetchedAt = fetchedAt; // eslint-disable-line no-underscore-dangle
if (account) {
// reset to latest account fetch date
_fetchedAt = account.fetchedAt || _fetchedAt;
// obtain latest billed date
const _bills = _.orderBy(account.bills, 'period.billedAt', 'desc'); // eslint-disable-line no-underscore-dangle
let { billedAt } = (_.first(_bills) || {}).period || {};
billedAt = billedAt || _fetchedAt;
billedAt = moment(billedAt)
.add(1, 'days')
.toDate();
// reset fetched date to latest billed date
_fetchedAt = _fetchedAt > billedAt ? billedAt : _fetchedAt;
}
Account.fetch(identity, _fetchedAt, function cb(error, fetched) {
next(error, account, fetched);
});
},
// 3.
function findAccountJurisdiction(account, fetched, next) {
// ensure fetched
let _fetched = _.merge({}, fetched); // eslint-disable-line no-underscore-dangle
_fetched = _.omitBy(_fetched, isEmpty);
const { jurisdiction } = _fetched;
// back off not fetched
if (_.isEmpty(_fetched) || _.isEmpty(jurisdiction)) {
next(null, account, _fetched);
}
// fetch jurisdiction
else {
// prepare criteria
const criteria = {
$or: [{ name: jurisdiction }, { code: jurisdiction }],
};
// fetch jurisdiction
Jurisdiction.findOne(criteria, function cb(error, data) {
_fetched.jurisdiction = data || account.jurisdiction;
next(error, account, _fetched);
});
}
},
// 4.
function upsertFetchedAccount(account, fetched, next) {
// ensure fetched
let _fetched = _.merge({}, fetched); // eslint-disable-line no-underscore-dangle
_fetched = _.omitBy(_fetched, isEmpty);
// upsert
if (account && account.post) {
// upsert accessors
_.forEach(_fetched.accessors, function cb(accessor) {
if (!_.isEmpty(accessor.phone)) {
account.upsertAccessor(accessor.phone, accessor);
}
});
// TODO bills upsert
// unset
delete _fetched.accessors;
// set other & patch
account.put(_fetched, next);
}
// create
else if (!_.isEmpty(_fetched)) {
Account.post(_fetched, next);
}
// invalid account number or identity
else {
const error = new Error('Invalid Account Number or Identity');
error.status = 400;
next(error);
}
},
],
done
);
};
/**
* @name getPhones
* @function getPhones
* @description pull distinct account phones
* @param {object} [criteria] valid query criteria
* @param {Function} done a callback to invoke on success or error
* @returns {string[]|Error} set of phone number or error
* @since 0.1.0
* @version 1.0.0
* @static
*/
AccountSchema.statics.getPhones = function getPhones(criteria, done) {
// refs
const Account = this;
// normalize arguments
const _criteria = _.isFunction(criteria) ? {} : _.merge({}, criteria); // eslint-disable-line no-underscore-dangle
const _done = _.isFunction(criteria) ? criteria : done; // eslint-disable-line no-underscore-dangle
return Account.find(_criteria)
.distinct('phone')
.exec(function onGetPhones(error, phones) {
let data = phones;
if (!error) {
data = _.uniq(_.compact([].concat(phones)));
}
return _done(error, data);
});
};
/* export account model */
var Account = model(MODEL_NAME_ACCOUNT, AccountSchema);
/* constants */
const API_VERSION = getString('API_VERSION', '1.0.0');
const PATH_VERIFY = '/accounts/verify';
const PATH_LIST = '/accounts';
const PATH_SINGLE = '/accounts/:id';
const PATH_ACCESSORS = '/accounts/:id/accessors';
const PATH_ACCESSORS_SINGLE = '/accounts/:id/accessors/:phone';
const PATH_JURISDICTION = '/jurisdictions/:jurisdiction/accounts';
/**
* @name AccountHttpRouter
* @namespace AccountHttpRouter
*
* @description A representation of an entity
* (i.e organization, individual, customer, or client) which
* receiving service(s) from a particular jurisdiction.
*
* @author Benson Maruchu <benmaruchu@gmail.com>
* @author lally elias <lallyelias87@gmail.com>
* @license MIT
* @since 0.1.0
* @version 1.0.0
* @public
*/
const router = new Router({
version: API_VERSION,
});
/**
* @name GetAccounts
* @memberof AccountHttpRouter
* @description Returns a list of accounts
*/
router.get(PATH_LIST, function getAccounts(request, response, next) {
// obtain request options
const options = _.merge({}, request.mquery);
Account.get(options, function onGetAccounts(error, results) {
// forward error
if (error) {
next(error);
}
// handle response
else {
response.status(200);
response.json(results);
}
});
});
/**
* @name PostAccount
* @memberof AccountHttpRouter
* @description Create new Account
*/
router.post(PATH_LIST, function postAccount(request, response, next) {
// obtain request body
const body = _.merge({}, request.body);
Account.post(body, function onPostAccount(error, created) {
// forward error
if (error) {
next(error);
}
// handle response
else {
response.status(201);
response.json(created);
}
});
});
/**
* @name GetAccount
* @memberof AccountHttpRouter
* @description Get existing account
*/
router.get(PATH_SINGLE, function getAccount(request, response, next) {
// obtain request options
const options = _.merge({}, request.mquery);
// obtain account id
options._id = request.params.id; // eslint-disable-line no-underscore-dangle
Account.getById(options, function onGetAccount(error, found) {
// forward error
if (error) {
next(error);
}
// handle response
else {
response.status(200);
response.json(found);
}
});
});
/**
* @name PatchAccount
* @memberof AccountHttpRouter
* @description Patch existing account
*/
router.patch(PATH_SINGLE, function patchAccount(request, response, next) {
// obtain account id
const _id = request.params.id; // eslint-disable-line no-underscore-dangle
// obtain request body
const patches = _.merge({}, request.body);
Account.patch(_id, patches, function onPatchAccount(error, patched) {
// forward error
if (error) {
next(error);
}
// handle response
else {
response.status(200);
response.json(patched);
}
});
});
/**
* @name PutAccount
* @memberof AccountHttpRouter
* @description Put existing account
*/
router.put(PATH_SINGLE, function putAc