UNPKG

auction

Version:
572 lines (483 loc) 11.7 kB
'use strict'; /** * Module dependencies. */ import { AuctionError } from './errors'; import Emitter from 'eventemitter3'; import predefine from 'predefine'; import dbg from 'debug'; import Bid from './bid'; import _ from 'lodash'; /** * Module variables. */ const debug = dbg('auction'); const noop = function noop(){}; const READ_ONLY_FIELDS = { authorization: true, auctionStatus: true, currentPrice: true, _events: true, outBid: true, bestBid: true, started: true, ended: true, bids: true }; export default class extends Emitter { /** * Auction constructor method. * * @param {Object} data * @param {Function} fn * @return {Auction} this * @api private */ constructor(data, fn) { super(); this.writable = predefine(this, predefine.WRITABLE); this.readable = predefine(this, predefine.READABLE); this.predefineProperties(); this.init(data, fn); } /** * Initialize `auction` object. * * @param {Object} data * @param {Function} fn * @return {Auction} this * @api private */ init(data = {}, fn) { let 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 */ predefineProperties() { this.writable('_events', {}); } /** * Merge 'auction' properties. * * @param {Object} data * @type {Auction} this * @api private */ mergeProperties(data) { for (let 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 */ mergeProperty(key, value) { if (READ_ONLY_FIELDS[key]) return; this[key] = value; } /** * Merge data to auction. * * @return {Object} data * @return {Boolean} * @api private */ merge(data) { this.mergeProperties(this.deff(data)); return this.data; } /** * Diff auction data. * * @return {Object} data * @return {Object} diff * @api private */ diff(data) { let res = {}; for (let key in data) { if (_.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 */ validate(data) { // Extend this method return false; } /** * Normalize auction data. * * @param {Object} data * @api private */ normalize(data) { // Extend this method return data; } /** * Check validation data. * * @param {Object} data * @return {Error|Undefined} * @api private */ check(data) { let message = null; if (!data.id) { message = 'Invalid auction ID.'; } else if (!_.isNumber(data.openPrice)) { message = 'Invalid open price.'; } else if ('minPrice' in data && !_.isNumber(data.minPrice)) { message = 'Invalid minimum price.'; } else { message = this.validate(data); } if (message) { let error = new 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 */ update(data, fn = noop) { data = this.normalize(data); let 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 */ destroy(fn = noop) { this.destroyed = true; this.removeAllListeners(); fn(); } /** * Reset `auction`. * * @return {Auction} this * @api private */ 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 */ start(data = {}, fn = noop) { let error = null; let agentId = data.agentId; let 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 || !_.isNumber(this.openPrice)) { error = 'Invalid opening price.'; } if (error) { debug('auction %d error %s', this.id, error); error = new 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(() => { fn(null, data); }); return this; } /** * Place `bid`. * * @param {Object} data * @param {Function} fn * @return {Auction} this * @api public */ bid(data = {}, fn = noop) { let bid = null; let error = null; let id = this.id; let bestBid = this.bestBid; let openPrice = this.openPrice; let agentId = data.agentId; let plus = this.increment; let status = this.auctionStatus; let omit = ['auctionId', 'saleId']; // Ensure that the first bid is always openPrice + 1 if (!this.bids.length) { plus = this.minIncrement; } bestBid = this.bestBid = _.omit(this.bestBid, omit); debug('creating bid %j', data); try { bid = new Bid({ 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 = _.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 AuctionError(error); this.emit('error', error); return setImmediate(fn.bind(null, error)); } debug(`bid placed ${bid.data}`); this.emit('changed', data); setImmediate(() => { fn(null, bid.data); }); return this; } /** * Ending `auction`. * * @param {Object} data * @param {Function} fn * @return {Auction} this * @api public */ ending(data = {}, fn = noop) { let error = null; let agentId = data.agentId; let 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 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(() => { fn(null, data); }); return this; } /** * End `auction`. * * @param {Object} data * @param {Function} fn * @return {Auction} this * @api public */ end(data = {}, fn = noop) { let error = null; let agentId = data.agentId; let 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 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(() => { fn(null, data); }); return this; } /** * Method to extend object. * * @param {Function} fn * @param {Object} options * @return {Auction} this * @api public */ use(fn, options) { fn(this, options); return this; } /** * Lazy get bid data. * * @type {Object} * @api public */ get currentPrice() { return _.isEmpty(this.bestBid) ? this.openPrice : this.bestBid.price; } /** * Lazy get auctioneer. * * @type {Object} * @api public */ get auctioneer() { this.started.agentId; } /** * Lazy get bid data. * * @type {Object} * @api public */ get data() { 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 }; } }