auction
Version:
Easy way to create auctions
660 lines (552 loc) • 17.2 kB
JavaScript
'use strict';
/**
* Module dependencies.
*/
Object.defineProperty(exports, '__esModule', {
value: true
});
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
var _get = function get(_x11, _x12, _x13) { var _again = true; _function: while (_again) { var object = _x11, property = _x12, receiver = _x13; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x11 = parent; _x12 = property; _x13 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _errors = require('./errors');
var _eventemitter3 = require('eventemitter3');
var _eventemitter32 = _interopRequireDefault(_eventemitter3);
var _predefine = require('predefine');
var _predefine2 = _interopRequireDefault(_predefine);
var _debug = require('debug');
var _debug2 = _interopRequireDefault(_debug);
var _bid = require('./bid');
var _bid2 = _interopRequireDefault(_bid);
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
/**
* Module variables.
*/
var debug = (0, _debug2['default'])('auction');
var noop = function noop() {};
var READ_ONLY_FIELDS = {
authorization: true,
auctionStatus: true,
currentPrice: true,
_events: true,
outBid: true,
bestBid: true,
started: true,
ended: true,
bids: true
};
var _default = (function (_Emitter) {
_inherits(_default, _Emitter);
/**
* Auction constructor method.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api private
*/
function _default(data, fn) {
_classCallCheck(this, _default);
_get(Object.getPrototypeOf(_default.prototype), 'constructor', this).call(this);
this.writable = (0, _predefine2['default'])(this, _predefine2['default'].WRITABLE);
this.readable = (0, _predefine2['default'])(this, _predefine2['default'].READABLE);
this.predefineProperties();
this.init(data, fn);
}
/**
* Initialize `auction` object.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api private
*/
_createClass(_default, [{
key: 'init',
value: function init(data, fn) {
if (data === undefined) data = {};
var error = this.check(data);
if (error) {
if (!this.listeners('error', true) && !fn) throw error;
if (fn) setImmediate(fn.bind(null, error));
this.emit('error', error);
return this;
}
this.id = data.id;
this.reset();
this.mergeProperties(data);
this.normalize(data);
debug('auction initialized %j', this.data);
if (fn) setImmediate(fn.bind(null, null, this.data));
return this;
}
/**
* Set predefined properties.
*
* @api private
*/
}, {
key: 'predefineProperties',
value: function predefineProperties() {
this.writable('_events', {});
}
/**
* Merge 'auction' properties.
*
* @param {Object} data
* @type {Auction} this
* @api private
*/
}, {
key: 'mergeProperties',
value: function mergeProperties(data) {
for (var key in data) {
if ('undefined' !== typeof data[key]) {
this.mergeProperty(key, data[key]);
}
}
}
/**
* Merge 'auction' property.
*
* @param {String} key
* @param {Mixed} value
* @type {Auction} this
* @api private
*/
}, {
key: 'mergeProperty',
value: function mergeProperty(key, value) {
if (READ_ONLY_FIELDS[key]) return;
this[key] = value;
}
/**
* Merge data to auction.
*
* @return {Object} data
* @return {Boolean}
* @api private
*/
}, {
key: 'merge',
value: function merge(data) {
this.mergeProperties(this.deff(data));
return this.data;
}
/**
* Diff auction data.
*
* @return {Object} data
* @return {Object} diff
* @api private
*/
}, {
key: 'diff',
value: function diff(data) {
var res = {};
for (var key in data) {
if (_lodash2['default'].isEqual(this[key], data[key])) continue;
if (!Order.READ_ONLY_FIELDS[key]) {
res[key] = data[key];
}
}
return res;
}
/**
* Validate auction data.
*
* @param {Object} data
* @return {Error|Boolean}
* @api private
*/
}, {
key: 'validate',
value: function validate(data) {
// Extend this method
return false;
}
/**
* Normalize auction data.
*
* @param {Object} data
* @api private
*/
}, {
key: 'normalize',
value: function normalize(data) {
// Extend this method
return data;
}
/**
* Check validation data.
*
* @param {Object} data
* @return {Error|Undefined}
* @api private
*/
}, {
key: 'check',
value: function check(data) {
var message = null;
if (!data.id) {
message = 'Invalid auction ID.';
} else if (!_lodash2['default'].isNumber(data.openPrice)) {
message = 'Invalid open price.';
} else if ('minPrice' in data && !_lodash2['default'].isNumber(data.minPrice)) {
message = 'Invalid minimum price.';
} else {
message = this.validate(data);
}
if (message) {
var error = new _errors.AuctionError(message);
debug('auction error %s', message);
this.emit('error', error);
return error;
}
}
/**
* Update `auction`.
*
* @param {Object} data
* @param {Function} fn
* @return {Order} this
* @api public
*/
}, {
key: 'update',
value: function update(data) {
var fn = arguments.length <= 1 || arguments[1] === undefined ? noop : arguments[1];
data = this.normalize(data);
var error = this.validate(data);
if (error) {
error = new OrderError(error);
this.emit('error', error);
return fn(error);
}
fn(null, this.merge(data));
}
/**
* Destroy `auction`.
*
* @param {Function} fn
* @return {Auction} this
* @api public
*/
}, {
key: 'destroy',
value: function destroy() {
var fn = arguments.length <= 0 || arguments[0] === undefined ? noop : arguments[0];
this.destroyed = true;
this.removeAllListeners();
fn();
}
/**
* Reset `auction`.
*
* @return {Auction} this
* @api private
*/
}, {
key: 'reset',
value: function reset() {
this.bids = [];
this.outBid = {};
this.bestBid = {};
this.started = {};
this.ended = {};
this.saleId = null;
this.saleDate = null;
this.minPrice = 0;
this.openPrice = 0;
this.increment = 1;
this.minIncrement = 1;
this.destroyed = false;
this.initialized = true;
this.auctionStatus = 'created';
return this;
}
/**
* Start `auction`.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api public
*/
}, {
key: 'start',
value: function start() {
var data = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var fn = arguments.length <= 1 || arguments[1] === undefined ? noop : arguments[1];
var error = null;
var agentId = data.agentId;
var status = this.auctionStatus;
this.openPrice = data.openPrice || this.openPrice;
if (!agentId) {
error = 'Invalid agent.';
} else if ('ending' === status) {
error = 'Auction is ending.';
} else if ('ended' === status) {
error = 'Auction already ended.';
} else if ('started' === status) {
error = 'Auction already started.';
} else if (!this.openPrice || !_lodash2['default'].isNumber(this.openPrice)) {
error = 'Invalid opening price.';
}
if (error) {
debug('auction %d error %s', this.id, error);
error = new _errors.AuctionError(error);
this.emit('error', error);
return setImmediate(fn.bind(null, error));
}
this.auctionStatus = 'started';
this.started = {
agentId: agentId,
timestamp: Date.now()
};
data = this.data;
debug('started auction %d', this.id, data);
this.emit('started', data);
setImmediate(function () {
fn(null, data);
});
return this;
}
/**
* Place `bid`.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api public
*/
}, {
key: 'bid',
value: function bid() {
var data = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var fn = arguments.length <= 1 || arguments[1] === undefined ? noop : arguments[1];
var bid = null;
var error = null;
var id = this.id;
var bestBid = this.bestBid;
var openPrice = this.openPrice;
var agentId = data.agentId;
var plus = this.increment;
var status = this.auctionStatus;
var omit = ['auctionId', 'saleId'];
// Ensure that the first bid is always openPrice + 1
if (!this.bids.length) {
plus = this.minIncrement;
}
bestBid = this.bestBid = _lodash2['default'].omit(this.bestBid, omit);
debug('creating bid %j', data);
try {
bid = new _bid2['default']({
price: data.price,
auctionId: this.id,
agentId: data.agentId,
saleId: this.saleId
});
} catch (e) {
debug('auction ' + id + ' error ' + e.messsage);
return process.nextTick(fn.bind(null, e));
}
debug('placing bid ' + bid.id);
if (!agentId) {
error = 'Invalid agent.';
} else if ('object' !== typeof bid) {
error = 'Invalid bid.';
} else if ('function' !== typeof bid.place) {
error = 'Invalid bid object.';
} else if ('created' === status) {
error = 'Auction not started.';
} else if ('ending' === status && this.ending && this.ending.agentId !== agentId) {
error = 'Auction has ended, waiting for Auctioneer to confirm and end auction.';
} else if ('ended' === status) {
error = 'Auction already ended.';
} else if (bid.price < bestBid.price + plus) {
error = 'Bid price ' + bid.price + ' must be ' + plus + ' higher than the current bid price $' + bestBid.price + '.';
} else if (bid.price <= this.openPrice) {
error = 'Bid price ' + bid.price + ' must be at least ' + plus + ' higher than the current bid price $' + openPrice + '.';
} else if (bestBid.price > bid.price) {
error = 'Invalid bid price';
}
if (!error) {
bid.accept();
this.outBid = bestBid;
this.bestBid = _lodash2['default'].omit(bid.data, omit);
this.bids.push(bid);
debug('saving bid ' + bid.id + ' to auction bid list');
} else {
bid.reject(error);
}
data = this.data;
if (error) {
debug('auction %d error %s', this.id, error);
error = new _errors.AuctionError(error);
this.emit('error', error);
return setImmediate(fn.bind(null, error));
}
debug('bid placed ' + bid.data);
this.emit('changed', data);
setImmediate(function () {
fn(null, bid.data);
});
return this;
}
/**
* Ending `auction`.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api public
*/
}, {
key: 'ending',
value: function ending() {
var data = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var fn = arguments.length <= 1 || arguments[1] === undefined ? noop : arguments[1];
var error = null;
var agentId = data.agentId;
var status = this.auctionStatus;
if (!agentId) {
error = 'Invalid agent.';
} else if ('ending' === status) {
error = 'Auction already ending.';
} else if ('ended' === status) {
error = 'Auction already ended.';
} else if ('started' !== status) {
error = 'Auction not started.';
}
if (error) {
debug('auction %d error %s', this.id, error);
error = new _errors.AuctionError(error);
this.emit('error', error);
return setImmediate(fn.bind(null, error));
}
this.auctionStatus = 'ending';
this.isending = {
agentId: agentId,
timestamp: Date.now()
};
data = this.data;
debug('ending auction %d', this.id, data);
this.emit('ending', data);
setImmediate(function () {
fn(null, data);
});
return this;
}
/**
* End `auction`.
*
* @param {Object} data
* @param {Function} fn
* @return {Auction} this
* @api public
*/
}, {
key: 'end',
value: function end() {
var data = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var fn = arguments.length <= 1 || arguments[1] === undefined ? noop : arguments[1];
var error = null;
var agentId = data.agentId;
var status = this.auctionStatus;
if (!agentId) {
error = 'Invalid agent.';
} else if ('ended' === status) {
error = 'Auction already ended.';
} else if ('started' !== status && 'ending' !== status) {
error = 'Auction not started.';
}
if (error) {
debug('auction %d error %s', this.id, error);
error = new _errors.AuctionError(error);
this.emit('error', error);
return setImmediate(fn.bind(null, error));
}
this.auctionStatus = 'ended';
this.ended = {
agentId: agentId,
timestamp: Date.now()
};
data = this.data;
debug('ended auction %d', this.id, data);
this.emit('ended', data);
setImmediate(function () {
fn(null, data);
});
return this;
}
/**
* Method to extend object.
*
* @param {Function} fn
* @param {Object} options
* @return {Auction} this
* @api public
*/
}, {
key: 'use',
value: function use(fn, options) {
fn(this, options);
return this;
}
/**
* Lazy get bid data.
*
* @type {Object}
* @api public
*/
}, {
key: 'currentPrice',
get: function get() {
return _lodash2['default'].isEmpty(this.bestBid) ? this.openPrice : this.bestBid.price;
}
/**
* Lazy get auctioneer.
*
* @type {Object}
* @api public
*/
}, {
key: 'auctioneer',
get: function get() {
this.started.agentId;
}
/**
* Lazy get bid data.
*
* @type {Object}
* @api public
*/
}, {
key: 'data',
get: function get() {
return {
id: this.id,
agents: this.agents,
auctioneer: this.auctioneer,
started: this.started,
ended: this.ended,
saleId: this.saleId,
saleDate: this.saleDate,
bids: this.bids,
outBid: this.outBid,
bestBid: this.bestBid,
minPrice: this.minPrice,
openPrice: this.openPrice,
currentPrice: this.currentPrice,
increment: this.increment,
auctionStatus: this.auctionStatus
};
}
}]);
return _default;
})(_eventemitter32['default']);
exports['default'] = _default;
module.exports = exports['default'];