Source: mongodb/models/advertiser.js

/**
 * @module mongodb/models/advertiser
 */
var mongoose = require('mongoose');
var deepPopulate = require('mongoose-deep-populate');
var Schema = mongoose.Schema;
var util = require('util');
var _ = require('underscore');
var mongooseApiQuery = require('mongoose-api-query');

var TreeDocument = require('./lib/treeDocument').TreeDocument;

// ---- Advertisers Collection Models ---- //

/**
 * Mongoose Schema representing Advertiser Creatives
 *
 * @class
 * @type {Schema}
 */
var creativeSchema = exports.creativeSchema = new Schema({
    name:               {type: String, required: true},
    active:             {type: Boolean, required: true, default: true},
    type:               String,
    tstamp:             {type: Date, default: Date.now},
    h:                  {type: Number, required: true},
    w:                  {type: Number, required: true},
    retina:             {type: Boolean, required: true, default: false},
    url:                {type: String, required: true },
    click_url:          {type: String, required: true},
    tag:                String,
    weight:             {type: Number, min: 0, max: 5, required: true, default: 1}
});
creativeSchema.plugin(deepPopulate, {});
creativeSchema.plugin(mongooseApiQuery);
var Creative = mongoose.model('Creative', creativeSchema);

// Virtual field to retrieve secure URL
creativeSchema.virtual('secureUrl').get(function(){
    if (this.url){
        return this.url.replace('http://', 'https://');
    }
});
creativeSchema.set('toObject', { virtuals: true });
creativeSchema.set('toJSON', { virtuals: true });

function heightValidator(val){
    var valid = true;
    this.creatives.forEach(function(creative){
        valid = creative.h == val;
    });
    return valid;
}
function widthValidator(val){
    var valid = true;
    this.creatives.forEach(function(creative){
        valid = creative.w == val;
    });
    return valid;
}

/**
 * Mongoose Schema representing Advertiser CreativeGroups
 *
 * @class
 * @type {Schema}
 */
var creativeGroupSchema = new Schema({
    name:               String,
    tstamp:             {type: Date, default: Date.now },
    active:             {type: Boolean, required: true, default: true},
    h:                  {type: Number, required: true, validate: heightValidator },
    w:                  {type: Number, required: true, validate: widthValidator },
    creatives:          [creativeSchema]
});
creativeGroupSchema.plugin(deepPopulate, {});
creativeGroupSchema.plugin(mongooseApiQuery);

/**
 * Randomly choose creative from this creative group according to
 * respective creative weights.
 *
 * @returns {Creative}
 */
creativeGroupSchema.methods.getWeightedRandomCreative = function(){
    var creatives = [];
    this.creatives.forEach(function(item){
        // filter on active flag
        if (item.active){
            for (var i= 0; i < item.weight; i++){
                creatives.push(item);
            }
        }
    });
    return creatives[Math.floor(Math.random()*creatives.length)]
};

var CreativeGroup = mongoose.model('CreativeGroup', creativeGroupSchema);

/**
 * Generates a Targeting Schema, which just consists of a target field
 * (ref to other model), number weight field, & optionally a
 * children array.  Used generically for a variety of targeting purposes
 * in Campaign model.
 *
 * @param ref String name of referenced model
 * @param [ref_type=Schema.ObjectId] Reference field type (default is ObjectId
 * @param [children] optional field definition to pass to Schema.children,
 *      typically of a different instantiation of WeightTargetingSchema
 * @returns {Schema}
 */
var WeightTargetingSchema = function(ref, ref_type, children){
    ref_type = ref_type || Schema.ObjectId;

    var schemaDefinition = {
        target: { type: ref_type, ref: ref },
        weight: { type: Number, max: 10, min: 0}
    };
    if (children) schemaDefinition.children = children;
    return new Schema(schemaDefinition);
};

/**
 *  Schemas to handle store Inventory Settings param
 *
 *  TODO: Nesting these is not going to be super-efficient when retrieving bid,
 *  TODO: especially if a lot of targets are set.  Willing to overlook for now but should
 *  TODO: be revisited.
 */
var placementTargetSchema = WeightTargetingSchema('Placement', Schema.ObjectId);
var pageTargetSchema = WeightTargetingSchema('Page', Schema.ObjectId, [placementTargetSchema]);
var siteTargetSchema = WeightTargetingSchema('Site', Schema.ObjectId, [pageTargetSchema]);
var inventoryTargetSchema = WeightTargetingSchema('Clique', String, [siteTargetSchema]);


/**
 * Generated a Block schema, which is similar to `WeightTargeting` schema except instead
 * of `weight`, `explicit` field is used to indicate whether blocking at this level
 * is to be enforced or ignored & assumed to be just an ancestor placeholder.
 */
var BlockSchema = function(ref, ref_type, children){
    ref_type = ref_type || Schema.ObjectId;
    var schemaDefinition = {
        target: { type: ref_type, ref: ref },
        //Currently, if explicit === true, then it will be blocked.
        //Otherwise, it's assumed to be an ancestor placeholder and will be skipped.
        //See statics.getInventoryBlockStatus for more details
        explicit: { type: Boolean, default: false }
    };
    if (children) schemaDefinition.children = children;
    return new Schema(schemaDefinition);
};
var placementBlockSchema = BlockSchema('Placement', Schema.ObjectId);
var pageBlockSchema = BlockSchema('Page', Schema.ObjectId, [placementBlockSchema]);
var siteBlockSchema = BlockSchema('Site', Schema.ObjectId, [pageBlockSchema]);
var inventoryBlockSchema = BlockSchema('Clique', String, [siteBlockSchema]);

/**
 * Campaign schema holds most targeting logic
 *
 * @class
 * @type {Schema}
 */
var campaignSchema = new Schema({
    name:               { type: String, required: true },
    active:             { type: Boolean, require: true, default: false },
    description:        String,
    tstamp:             { type: Date, default: Date.now },
    budget:             { type: Number, required: true },
    start_date:         { type: Date, required: true },
    end_date:           { type: Date },
    even_pacing:        { type: Boolean, default: true },
    max_bid:            { type: Number, required: true },
    base_bid:           { type: Number, required: true },
    // TO BE DEPRECATED
    placement_targets:  [WeightTargetingSchema('Placement', Schema.ObjectId)],
    dma_targets:        [WeightTargetingSchema('DMA', Number)],
    country_targets:    [WeightTargetingSchema('Country', String)],
    // TO BE DEPRECATED
    blocked_cliques:    [{ type: String, ref: 'Clique' }],
    inventory_targets:  [inventoryTargetSchema],
    blocked_inventory:  [inventoryBlockSchema],
    frequency:          { type: Number, default: 64 },
    creativegroups:     [creativeGroupSchema],
    clique:             { type: String, required: true, ref: 'Clique' }//TODO: Add validators to check string against Clique._id
});

/**
 * Gets weighting for node based on inventory_targets setting based on
 * parent-child inheritance w/ child override.
 *
 * !!NOTE!! This was written as a static method so that it could be easily
 * serialized/deserialized to pass to BidAgent instances running in a primitive
 * Node environment.  As such, any modifications to this must not rely on any Node
 * modules or libraries, only native ECMAScript functions & objects.
 *
 * @param branch array of ids representing branch, where top level = Clique & bottom = placement
 *      Ex: ['Outdoor', '9ruhu9302jnu9nnj','83hdnddknd678dua','83uhcbdbedndb24']
 * @param inventory_targets any valid instance of inventoryTargetSchema
 */
campaignSchema.statics.getInventoryWeight = function(branch, inventory_targets){
    if (!inventory_targets) return 1;
    function _inner(branch, subtree, parentWeight){
        var weight = parentWeight || 1;
        var nodeId = branch.shift();
        var targetObj = subtree.filter(function(elem){ return elem.target == nodeId; })[0];
        if (targetObj){
            if (targetObj.weight !== null){
                // override parent weight w/ child
                weight = targetObj.weight;
            }
            if (targetObj.children){
                if (targetObj.children.length > 0){
                    weight = _inner(branch, targetObj.children, weight);
                }
            }
        }
        return weight;
    }
    return _inner(branch, inventory_targets);
};

/**
 * Walks blocked_inventory tree setting to determine if branch is blocked.
 *
 * Currently, a branch is deemed to be "blocked" based on the 'explicit' setting of
 * its nearest ancestor (including the node itself) stored in blocked_inventory.
 *
 * For example:
 * ```
 * $ var branch = ['Outdoor','123','456','789'];
 * $ var blocked_inventory = [{
 *                              target: Outdoor,
 *                              explicit = false,
 *                              children: [{
 *                                  target: '123',
 *                                  children: null //no other children are stored
 *                                  explicit: true //nearest ancestor in stored branch set to true
 *                              }]
 *                         }];
 * $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
 * true
 *
 * $ blocked_inventory = [{
 *                          target: Outdoor,
 *                          explicit = false,
 *                          children: [{
 *                              target: '123',
 *                              explicit: true
 *                              children: [{
 *                                    target: '456'
 *                                    explicit: false, //parent is set to false
 *                                    children: null //no other children are stored
 *                              }]
 *                          }]
 *                       }];
 * $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
 * false
 *
 * $ blocked_inventory = [{
 *                          target: Outdoor,
 *                          explicit = false,
 *                          children: [{
 *                              target: '123',
 *                              explicit: false,
 *                              children: [{
 *                                  target: '456'
 *                                  explicit: false, //parent is set to false
 *                                  children: [{
 *                                      target: '789',
 *                                      explicit: true, // lowest level node in branch is stored w/ explicit = true
 *                                      children: null
 *                                  }]
 *                              }]
 *                          }]
 *                       }];
 * $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
 * true
 * ```
 *
 * **NOTE** This was written as a static method so that it could be easily
 * serialized/deserialized to pass to BidAgent instances running in a primitive
 * Node environment.  As such, any modifications to this must not rely on any Node
 * modules or libraries, only native ECMAScript functions & objects.
 *
 * @param {Array} branch array of ids representing branch, where top level = Clique & bottom = placement
 *      Ex: `['Outdoor', '9ruhu9302jnu9nnj','83hdnddknd678dua','83uhcbdbedndb24']`
 * @param {inventoryBlockSchema} blocked_inventory any valid instance of inventoryBlockSchema
 */
campaignSchema.statics.getInventoryBlockStatus = function(branch, blocked_inventory){
    if (!blocked_inventory) return false;
    function _inner(branch, subtree, parentBlocked){
        var isBlocked = parentBlocked || false;
        var nodeId = branch.shift();
        if (nodeId){
            var targetObj = subtree.filter(function(elem){ return elem.target == nodeId; })[0];
            if (targetObj){
                // Set isBlocked based on `explicit` boolean param in block schema
                // Parent objects
                isBlocked = targetObj.explicit;
                if (targetObj.children){
                    if (targetObj.children.length > 0){
                        isBlocked = _inner(branch, targetObj.children, isBlocked);
                    }
                }
            }
        }
        return isBlocked;
    }
    return _inner(branch, blocked_inventory);
};

campaignSchema.plugin(deepPopulate, {});
campaignSchema.plugin(mongooseApiQuery);
var Campaign = mongoose.model('Campaign', campaignSchema);

/**
 * Action Beacon schema, representing beacons generated by advertisers to
 * track downstream actions.
 * @class
 * @type {Schema}
 */
var actionBeaconSchema = new Schema({
    name:           { type: String, required: true},
    click_lookback: { type: Number, default: 30 }, // this won't do anything for now, building in to future-proof
    view_lookback:  { type: Number, default: 15 }, // this won't do anything for now, building in to future-proof
    match_view:     { type: Boolean, default: true }, // this won't do anything for now, building in to future-proof
    match_click:    { type: Boolean, default: true } // this won't do anything for now, building in to future-proof
});
actionBeaconSchema.plugin(mongooseApiQuery);
var ActionBeacon = mongoose.model('ActionBeacon', actionBeaconSchema);

/**
 * Advertiser schema, top-level container object.
 *
 * @class
 * @type {Schema}
 */
var advertiserSchema = new Schema({
    name:               { type: String, required: true, index: true },
    user:               [{ type: Schema.ObjectId, ref: 'User' }],
    organization:       { type: Schema.ObjectId, ref: 'Organization' },
    description:        String,
    logo_url:           String,
    nonprofit:          { type: Boolean, require: true, default: false },
    advertiser_fee:     { type: Number, required: true, default: 0.10 },
    website:            { type: String, required: true },
    tstamp:             { type: Date, default: Date.now },
    currency:           { type: String, default: 'USD' },
    campaigns:          [campaignSchema],
    actionbeacons:      [actionBeaconSchema],
    cliques:            [{ type: String, required: true, ref: 'Clique' }] //TODO: Add validators to check string against Clique._id
});
// Virtual field to retrieve secure URL
advertiserSchema.virtual('logo_secure_url').get(function(){
    if (this.logo_url){
        return this.logo_url.replace('http://', 'https://');
    }
});
advertiserSchema.set('toObject', { virtuals: true });
advertiserSchema.set('toJSON', { virtuals: true });
advertiserSchema.plugin(deepPopulate, {});
advertiserSchema.plugin(mongooseApiQuery);
advertiserSchema.set('autoIndex', false);
var Advertiser = mongoose.model('Advertiser', advertiserSchema);

/**
 * TreeDocument subclass wrapping Advertiser document tree.
 *
 * @param {mongoose.connection} connection DB connection object
 * @param {Object} options
 * @constructor
 */
var AdvertiserModels = function(connection, options){
    options = options || {};
    this.connection = connection;
    this.ActionBeacon = this.connection.model('ActionBeacon');
    this.Advertiser = this.connection.model('Advertiser');
    this.Campaign = this.connection.model('Campaign');
    this.Creative = this.connection.model('Creative');
    this.CreativeGroup = this.connection.model('CreativeGroup');

    var branches = [['Campaign', 'CreativeGroup','Creative'], ['ActionBeacon']];
    var top_level_node = 'Advertiser';
    TreeDocument.call(this, top_level_node, branches, options);
};
util.inherits(AdvertiserModels, TreeDocument);
exports.AdvertiserModels = AdvertiserModels;