<commit_before>const Gdax = require('gdax');
const _ = require('lodash');
const moment = require('moment');

const errors = require('../exchangeErrors');
const retry = require('../exchangeUtils').retry;

const BATCH_SIZE = 100;
const QUERY_DELAY = 350;

const Trader = function(config) {
  this.post_only = true;
  this.use_sandbox = false;
  this.name = 'GDAX';
  this.scanback = false;
  this.scanbackTid = 0;
  this.scanbackResults = [];
  this.asset = config.asset;
  this.currency = config.currency;

  this.api_url = 'https://api.gdax.com';
  this.api_sandbox_url = 'https://api-public.sandbox.gdax.com';

  if (_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.passphrase = config.passphrase;

    this.pair = [config.asset, config.currency].join('-').toUpperCase();
    this.post_only =
      typeof config.post_only !== 'undefined' ? config.post_only : true;

    if (config.sandbox) {
      this.use_sandbox = config.sandbox;
    }

  }

  this.gdax_public = new Gdax.PublicClient(
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
  this.gdax = new Gdax.AuthenticatedClient(
    this.key,
    this.secret,
    this.passphrase,
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
};

const recoverableErrors = [
  'SOCKETTIMEDOUT',
  'TIMEDOUT',
  'CONNRESET',
  'CONNREFUSED',
  'NOTFOUND',
  'Rate limit exceeded',
  'Response code 5',
  'GDAX is currently under maintenance.',
  'HTTP 408 Error',
  'HTTP 504 Error',
  'HTTP 503 Error',
  'socket hang up',
  'EHOSTUNREACH',
  'EAI_AGAIN',
  'ENETUNREACH'
];

const includes = (str, list) => {
  if(!_.isString(str))
    return false;

  return _.some(list, item => str.includes(item));
}

Trader.prototype.processResponse = function(method, next) {
  return (error, response, body) => {
    if(!error && body && !_.isEmpty(body.message)) {
      error = new Error(body.message);
    }

    if(
      response &&
      response.statusCode < 200 &&
      response.statusCode >= 300
    ) {
      error = new Error(`Response code ${response.statusCode}`);
    }

    if(error) {
      if(includes(error.message, recoverableErrors)) {
        error.notFatal = true;
      }

      if(
        ['buy', 'sell'].includes(method) &&
        error.message.includes('Insufficient funds')
      ) {
        error.retry = 10;
      }

      console.log(error.message);

      return next(error);
    }

    return next(undefined, body);
  }
}

Trader.prototype.getPortfolio = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    var portfolio = data.map(function(account) {
      return {
        name: account.currency.toUpperCase(),
        amount: parseFloat(account.available),
      };
    });
    callback(undefined, portfolio);
  };

  const fetch = cb =>
    this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTicker = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);
    callback(undefined, { bid: +data.bid, ask: +data.ask });
  };

  const fetch = cb =>
    this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
  retry(null, fetch, result);
};

Trader.prototype.getFee = function(callback) {
  //https://www.gdax.com/fees
  // const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
  const fee = 0;

  //There is no maker fee, not sure if we need taker fee here
  //If post only is enabled, gdax only does maker trades which are free
  callback(undefined, this.post_only ? 0 : fee);
};

Trader.prototype.roundPrice = function(price) {
  return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
}

Trader.prototype.roundAmount = function(amount) {
  return this.getMaxDecimalsNumber(amount);
}

Trader.prototype.buy = function(amount, price, callback) {
  const buyParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({buyParams}, err.message);
      return callback(err);
    }
    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.buy(buyParams, this.processResponse('buy', cb));
  retry(null, fetch, result);
};

Trader.prototype.sell = function(amount, price, callback) {
  const sellParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({sellParams}, err.message);
      return callback(err);
    }

    if(data.message && data.message.includes('Insufficient funds')) {
      err = new Error(data.message);
      err.retryOnce = true;
      return callback(err);
    }

    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.sell(sellParams, this.processResponse('sell', cb));
  retry(null, fetch, result);
};

Trader.prototype.checkOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    // @link:
    // https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
    var status = data.status;
    if(status == 'pending') {
      // technically not open yet, but will be soon
      return callback(undefined, { executed: false, open: true, filledAmount: 0 });
    } if (status === 'done' || status === 'settled') {
      return callback(undefined, { executed: true, open: false });
    } else if (status === 'rejected') {
      return callback(undefined, { executed: false, open: false });
    } else if(status === 'open' || status === 'active') {
      return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
    }

    callback(new Error('Unknown status ' + status));
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    const price = parseFloat(data.price);
    const amount = parseFloat(data.filled_size);
    const date = moment(data.done_at);
    const fees = {
      // you always pay fee in the base currency on gdax
      [this.currency]: +data.fill_fees
    }
    const feePercent = +data.fill_fees / price / amount * 100;

    callback(undefined, { price, amount, date, fees, feePercent });
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('getOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.cancelOrder = function(order, callback) {
  // callback for cancelOrder should be true if the order was already filled, otherwise false
  const result = (err, data) => {
    if(err) {
      return callback(null, true);  // need to catch the specific error but usually an error on cancel means it was filled
    }

    return callback(null, false);
  };

  const fetch = cb =>
    this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTrades = function(since, callback, descending) {
  var lastScan = 0;

  const handle = function(err, data) {
    if (err) return callback(err);

    var result = _.map(data, function(trade) {
      return {
        tid: trade.trade_id,
        amount: parseFloat(trade.size),
        date: moment.utc(trade.time).format('X'),
        price: parseFloat(trade.price),
      };
    });

    if (this.scanback) {
      var last = _.last(data);
      var first = _.first(data);

      // Try to find trade id matching the since date
      if (!this.scanbackTid) {
        // either scan for new ones or we found it.
        if (moment.utc(last.time) < moment.utc(since)) {
          this.scanbackTid = last.trade_id;
        } else {
          console.log('Scanning backwards...' + last.time);
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                {
                  after: last.trade_id - BATCH_SIZE * lastScan,
                  limit: BATCH_SIZE,
                },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
          lastScan++;
          if (lastScan > 100) {
            lastScan = 10;
          }
        }
      }

      if (this.scanbackTid) {
        // if scanbackTid is set we need to move forward again
        console.log(
          'Backwards: ' +
            last.time +
            ' (' +
            last.trade_id +
            ') to ' +
            first.time +
            ' (' +
            first.trade_id +
            ')'
        );

        this.scanbackResults = this.scanbackResults.concat(result.reverse());

        if (this.scanbackTid != first.trade_id) {
          this.scanbackTid = first.trade_id;
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
        } else {
          this.scanback = false;
          this.scanbackTid = 0;

          console.log('Scan finished: data found:' + this.scanbackResults.length);
          callback(null, this.scanbackResults);

          this.scanbackResults = [];
        }
      }
    } else {
      callback(null, result.reverse());
    }
  };

  if (since || this.scanback) {
    this.scanback = true;
    if (this.scanbackTid) {
      let handler = cb =>
        this.gdax_public.getProductTrades(
          this.pair,
          { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
          this.processResponse('getTrades', cb)
        );
      retry(
        retryForever,
        _.bind(handler, this),
        _.bind(process, this)
      );
    } else {
      console.log('Scanning back in the history needed...', since);
    }
  }

  const fetch = cb =>
    this.gdax_public.getProductTrades(
      this.pair,
      { limit: BATCH_SIZE },
      this.processResponse('getTrades', cb)
    );
  retry(null, fetch, handle);
};

Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
  var decimalNumber = parseFloat(number);

  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
    .length;

  var decimalMultiplier = 1;
  for (i = 0; i < decimalLimit; i++) {
    decimalMultiplier *= 10;
  }

  return decimalCount <= decimalLimit
    ? decimalNumber.toString()
    : (
        Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
      ).toFixed(decimalLimit);
};

Trader.getCapabilities = function() {
  return {
    name: 'GDAX',
    slug: 'gdax',
    currencies: ['USD', 'EUR', 'GBP', 'BTC'],
    assets: ['BTC', 'LTC', 'ETH', 'BCH'],
    markets: [
      { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['USD', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
    ],
    requires: ['key', 'secret', 'passphrase'],
    providesHistory: 'date',
    providesFullHistory: true,
    tid: 'tid',
    tradable: true,
    forceReorderDelay: false,
    gekkoBroker: 0.6
  };
};

module.exports = Trader;
<commit_msg>Added ETC support for GDAX<commit_after>const Gdax = require('gdax');
const _ = require('lodash');
const moment = require('moment');

const errors = require('../exchangeErrors');
const retry = require('../exchangeUtils').retry;

const BATCH_SIZE = 100;
const QUERY_DELAY = 350;

const Trader = function(config) {
  this.post_only = true;
  this.use_sandbox = false;
  this.name = 'GDAX';
  this.scanback = false;
  this.scanbackTid = 0;
  this.scanbackResults = [];
  this.asset = config.asset;
  this.currency = config.currency;

  this.api_url = 'https://api.gdax.com';
  this.api_sandbox_url = 'https://api-public.sandbox.gdax.com';

  if (_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.passphrase = config.passphrase;

    this.pair = [config.asset, config.currency].join('-').toUpperCase();
    this.post_only =
      typeof config.post_only !== 'undefined' ? config.post_only : true;

    if (config.sandbox) {
      this.use_sandbox = config.sandbox;
    }

  }

  this.gdax_public = new Gdax.PublicClient(
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
  this.gdax = new Gdax.AuthenticatedClient(
    this.key,
    this.secret,
    this.passphrase,
    this.use_sandbox ? this.api_sandbox_url : undefined
  );
};

const recoverableErrors = [
  'SOCKETTIMEDOUT',
  'TIMEDOUT',
  'CONNRESET',
  'CONNREFUSED',
  'NOTFOUND',
  'Rate limit exceeded',
  'Response code 5',
  'GDAX is currently under maintenance.',
  'HTTP 408 Error',
  'HTTP 504 Error',
  'HTTP 503 Error',
  'socket hang up',
  'EHOSTUNREACH',
  'EAI_AGAIN',
  'ENETUNREACH'
];

const includes = (str, list) => {
  if(!_.isString(str))
    return false;

  return _.some(list, item => str.includes(item));
}

Trader.prototype.processResponse = function(method, next) {
  return (error, response, body) => {
    if(!error && body && !_.isEmpty(body.message)) {
      error = new Error(body.message);
    }

    if(
      response &&
      response.statusCode < 200 &&
      response.statusCode >= 300
    ) {
      error = new Error(`Response code ${response.statusCode}`);
    }

    if(error) {
      if(includes(error.message, recoverableErrors)) {
        error.notFatal = true;
      }

      if(
        ['buy', 'sell'].includes(method) &&
        error.message.includes('Insufficient funds')
      ) {
        error.retry = 10;
      }

      console.log(error.message);

      return next(error);
    }

    return next(undefined, body);
  }
}

Trader.prototype.getPortfolio = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    var portfolio = data.map(function(account) {
      return {
        name: account.currency.toUpperCase(),
        amount: parseFloat(account.available),
      };
    });
    callback(undefined, portfolio);
  };

  const fetch = cb =>
    this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTicker = function(callback) {
  const result = (err, data) => {
    if (err) return callback(err);
    callback(undefined, { bid: +data.bid, ask: +data.ask });
  };

  const fetch = cb =>
    this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
  retry(null, fetch, result);
};

Trader.prototype.getFee = function(callback) {
  //https://www.gdax.com/fees
  // const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
  const fee = 0;

  //There is no maker fee, not sure if we need taker fee here
  //If post only is enabled, gdax only does maker trades which are free
  callback(undefined, this.post_only ? 0 : fee);
};

Trader.prototype.roundPrice = function(price) {
  return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
}

Trader.prototype.roundAmount = function(amount) {
  return this.getMaxDecimalsNumber(amount);
}

Trader.prototype.buy = function(amount, price, callback) {
  const buyParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({buyParams}, err.message);
      return callback(err);
    }
    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.buy(buyParams, this.processResponse('buy', cb));
  retry(null, fetch, result);
};

Trader.prototype.sell = function(amount, price, callback) {
  const sellParams = {
    price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
    size: this.getMaxDecimalsNumber(amount),
    product_id: this.pair,
    post_only: this.post_only,
  };

  const result = (err, data) => {
    if (err) {
      console.log({sellParams}, err.message);
      return callback(err);
    }

    if(data.message && data.message.includes('Insufficient funds')) {
      err = new Error(data.message);
      err.retryOnce = true;
      return callback(err);
    }

    callback(undefined, data.id);
  };

  const fetch = cb =>
    this.gdax.sell(sellParams, this.processResponse('sell', cb));
  retry(null, fetch, result);
};

Trader.prototype.checkOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    // @link:
    // https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
    var status = data.status;
    if(status == 'pending') {
      // technically not open yet, but will be soon
      return callback(undefined, { executed: false, open: true, filledAmount: 0 });
    } if (status === 'done' || status === 'settled') {
      return callback(undefined, { executed: true, open: false });
    } else if (status === 'rejected') {
      return callback(undefined, { executed: false, open: false });
    } else if(status === 'open' || status === 'active') {
      return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
    }

    callback(new Error('Unknown status ' + status));
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getOrder = function(order, callback) {
  const result = (err, data) => {
    if (err) return callback(err);

    const price = parseFloat(data.price);
    const amount = parseFloat(data.filled_size);
    const date = moment(data.done_at);
    const fees = {
      // you always pay fee in the base currency on gdax
      [this.currency]: +data.fill_fees
    }
    const feePercent = +data.fill_fees / price / amount * 100;

    callback(undefined, { price, amount, date, fees, feePercent });
  };

  const fetch = cb =>
    this.gdax.getOrder(order, this.processResponse('getOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.cancelOrder = function(order, callback) {
  // callback for cancelOrder should be true if the order was already filled, otherwise false
  const result = (err, data) => {
    if(err) {
      return callback(null, true);  // need to catch the specific error but usually an error on cancel means it was filled
    }

    return callback(null, false);
  };

  const fetch = cb =>
    this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
  retry(null, fetch, result);
};

Trader.prototype.getTrades = function(since, callback, descending) {
  var lastScan = 0;

  const handle = function(err, data) {
    if (err) return callback(err);

    var result = _.map(data, function(trade) {
      return {
        tid: trade.trade_id,
        amount: parseFloat(trade.size),
        date: moment.utc(trade.time).format('X'),
        price: parseFloat(trade.price),
      };
    });

    if (this.scanback) {
      var last = _.last(data);
      var first = _.first(data);

      // Try to find trade id matching the since date
      if (!this.scanbackTid) {
        // either scan for new ones or we found it.
        if (moment.utc(last.time) < moment.utc(since)) {
          this.scanbackTid = last.trade_id;
        } else {
          console.log('Scanning backwards...' + last.time);
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                {
                  after: last.trade_id - BATCH_SIZE * lastScan,
                  limit: BATCH_SIZE,
                },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
          lastScan++;
          if (lastScan > 100) {
            lastScan = 10;
          }
        }
      }

      if (this.scanbackTid) {
        // if scanbackTid is set we need to move forward again
        console.log(
          'Backwards: ' +
            last.time +
            ' (' +
            last.trade_id +
            ') to ' +
            first.time +
            ' (' +
            first.trade_id +
            ')'
        );

        this.scanbackResults = this.scanbackResults.concat(result.reverse());

        if (this.scanbackTid != first.trade_id) {
          this.scanbackTid = first.trade_id;
          setTimeout(() => {
            let handler = cb =>
              this.gdax_public.getProductTrades(
                this.pair,
                { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
                this.processResponse('getTrades', cb)
              );
            retry(
              retryForever,
              _.bind(handler, this),
              _.bind(process, this)
            );
          }, QUERY_DELAY);
        } else {
          this.scanback = false;
          this.scanbackTid = 0;

          console.log('Scan finished: data found:' + this.scanbackResults.length);
          callback(null, this.scanbackResults);

          this.scanbackResults = [];
        }
      }
    } else {
      callback(null, result.reverse());
    }
  };

  if (since || this.scanback) {
    this.scanback = true;
    if (this.scanbackTid) {
      let handler = cb =>
        this.gdax_public.getProductTrades(
          this.pair,
          { after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
          this.processResponse('getTrades', cb)
        );
      retry(
        retryForever,
        _.bind(handler, this),
        _.bind(process, this)
      );
    } else {
      console.log('Scanning back in the history needed...', since);
    }
  }

  const fetch = cb =>
    this.gdax_public.getProductTrades(
      this.pair,
      { limit: BATCH_SIZE },
      this.processResponse('getTrades', cb)
    );
  retry(null, fetch, handle);
};

Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
  var decimalNumber = parseFloat(number);

  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
    .length;

  var decimalMultiplier = 1;
  for (i = 0; i < decimalLimit; i++) {
    decimalMultiplier *= 10;
  }

  return decimalCount <= decimalLimit
    ? decimalNumber.toString()
    : (
        Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
      ).toFixed(decimalLimit);
};

Trader.getCapabilities = function() {
  return {
    name: 'GDAX',
    slug: 'gdax',
    currencies: ['USD', 'EUR', 'GBP', 'BTC'],
    assets: ['BTC', 'LTC', 'ETH', 'BCH', 'ETC'],
    markets: [
      { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['USD', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['USD', 'ETC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['EUR', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['EUR', 'ETC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
      { pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
      { pair: ['BTC', 'ETC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
    ],
    requires: ['key', 'secret', 'passphrase'],
    providesHistory: 'date',
    providesFullHistory: true,
    tid: 'tid',
    tradable: true,
    forceReorderDelay: false,
    gekkoBroker: 0.6
  };
};

module.exports = Trader;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage } from 'react-intl';
import { get } from 'lodash';
import Markdown from 'react-markdown';

import withIntl from '../../../lib/withIntl';
import { getCurrencySymbol } from '../../../lib/utils';

import InputField from '../../../components/InputField';
import SignInForm from '../../../components/SignInForm';
import categories from '../../../constants/categories';
import Button from '../../../components/Button';

class CreateExpenseForm extends React.Component {
  static propTypes = {
    collective: PropTypes.object,
    LoggedInUser: PropTypes.object,
    onSubmit: PropTypes.func,
  };

  constructor(props) {
    super(props);
    this.getOptions = this.getOptions.bind(this);
    this.renderForm = this.renderForm.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);

    this.messages = defineMessages({
      paypal: {
        id: 'expense.payoutMethod.paypal',
        defaultMessage: 'PayPal ({paypalEmail, select, missing {missing} other {{paypalEmail}}})',
      },
      'newExpense.paypal.label': {
        id: 'newExpense.paypal.label',
        defaultMessage: 'Please provide address',
      },
      other: {
        id: 'expense.payoutMethod.manual',
        defaultMessage: 'Other (see instructions)',
      },
      donation: {
        id: 'expense.payoutMethod.donation',
        defaultMessage: 'Donation',
      },
      'error.descriptionMissing': {
        id: 'expense.error.descriptionMissing',
        defaultMessage: 'Missing description',
      },
      'error.amountMissing': {
        id: 'expense.error.amountMissing',
        defaultMessage: 'Amount must be greater than 0',
      },
      'error.privateMessageMissing': {
        id: 'expense.error.privateMessageMissing',
        defaultMessage: "Please provide instructions on how you'd like to be reimbursed as a private note",
      },
      'error.paypalEmailMissing': {
        id: 'expense.error.paypalEmailMissing',
        defaultMessage: 'Please provide your PayPal email address (or change the payout method)',
      },
      'error.attachmentMissing': {
        id: 'expense.error.attachmentMissing',
        defaultMessage: 'Missing attachment',
      },
    });

    this.categoriesOptions = categories(props.collective.slug).map(category => {
      return { [category]: category };
    });

    this.state = {
      modified: false,
      expense: {
        category: Object.keys(this.categoriesOptions[0])[0],
        payoutMethod: 'paypal',
        paypalEmail: (props.LoggedInUser && props.LoggedInUser.paypalEmail) || null,
      },
      isExpenseValid: false,
      loading: false,
    };
  }

  getOptions(arr, intlVars) {
    return arr.map(key => {
      const obj = {};
      obj[key] = this.props.intl.formatMessage(this.messages[key], intlVars);
      return obj;
    });
  }

  validate(expense) {
    const { intl } = this.props;
    if (!expense.description) {
      this.setState({
        error: intl.formatMessage(this.messages['error.descriptionMissing']),
      });
      return false;
    }
    if (!expense.amount > 0) {
      this.setState({
        error: intl.formatMessage(this.messages['error.amountMissing']),
      });
      return false;
    }
    if (!expense.attachment) {
      this.setState({
        error: intl.formatMessage(this.messages['error.attachmentMissing']),
      });
      return false;
    }
    if (expense.payoutMethod === 'other' && !expense.privateMessage) {
      this.setState({
        error: intl.formatMessage(this.messages['error.privateMessageMissing']),
      });
      return false;
    }
    if (expense.payoutMethod === 'paypal' && !expense.paypalEmail) {
      this.setState({
        error: intl.formatMessage(this.messages['error.paypalEmailMissing']),
      });
      return false;
    }
    this.setState({ error: null });
    return true;
  }

  handleChange(attr, value) {
    const expense = {
      ...this.state.expense,
      [attr]: value,
    };
    const newState = {
      modified: true,
      expense,
      isExpenseValid: this.validate(expense),
    };
    this.setState(newState);
    this.props.onChange && this.props.onChange(expense);
  }

  UNSAFE_componentWillReceiveProps(newProps) {
    if (!this.props.LoggedInUser && newProps.LoggedInUser && !this.state.expense.paypalEmail) {
      this.handleChange('paypalEmail', newProps.LoggedInUser.paypalEmail);
    }
  }

  async onSubmit(e) {
    if (e) {
      e.preventDefault();
    }
    try {
      await this.props.onSubmit(this.state.expense);
      this.setState({
        modified: false,
        isExpenseValid: false,
        expense: {},
        loading: false,
      });
    } catch (e) {
      this.setState({ loading: false });
      console.error('CreateExpenseForm onSubmit error', e);
    }
    return false;
  }

  renderForm() {
    const { LoggedInUser, intl, collective } = this.props;
    const { expense } = this.state;

    const payoutMethods = this.getOptions(['paypal', 'other', 'donation'], {
      paypalEmail: get(expense, 'user.paypalEmail') || intl.formatMessage(this.messages['newExpense.paypal.label']),
    });

    return (
      <div className={`CreateExpenseForm ${this.props.mode}`}>

        <form onSubmit={this.onSubmit}>
          {!collective.expensePolicy && LoggedInUser && LoggedInUser.canEditCollective(collective) && (

                <FormattedMessage id="collective.expensePolicy.label" defaultMessage="Expense policy" />

                <FormattedMessage
                  id="collective.expensePolicy.description"
                  defaultMessage="It can be daunting for the community to file an expense. Help them by providing a clear expense policy to explain what they can expense."
                />

              <Button className="blue" href={`/${collective.slug}/edit/expenses`}>
                <FormattedMessage id="expense.expensePolicy.add" defaultMessage="add an expense policy" />
              </Button>

          )}
          {(collective.expensePolicy || get(collective, 'host.expensePolicy')) && (

                <FormattedMessage id="expense.expensePolicy" defaultMessage="Expense policy" />

              {collective.expensePolicy && <Markdown source={collective.expensePolicy} />}
              {get(collective, 'host.expensePolicy') && <Markdown source={get(collective, 'host.expensePolicy')} />}

          )}

            <FormattedMessage
              id="expense.disclaimer"
              defaultMessage="You must upload a valid receipt or invoice. We should be able to see clearly on the picture (or PDF) the total amount paid, the date, the items purchased and the legal address."
            />

              <FormattedMessage id="needHelp" defaultMessage="Need help?" />

              <InputField
                type="dropzone"
                options={{ accept: 'image/jpeg, image/png, application/pdf' }}
                name="attachment"
                className="attachmentField"
                onChange={attachment => this.handleChange('attachment', attachment)}
                defaultValue={expense.attachment}
                placeholder={'/static/images/receipt.svg'}
                description={
                  <FormattedMessage
                    id="expense.attachment.description"
                    defaultMessage="Upload receipt or invoice (photo or PDF)"
                  />
                }
              />

                  <FormattedMessage id="expense.description" defaultMessage="description" />

                    <InputField
                      type="text"
                      name="description"
                      defaultValue={expense.description}
                      className="descriptionField"
                      maxLength={255}
                      onChange={description => this.handleChange('description', description)}
                    />

                <FormattedMessage id="expense.amount" defaultMessage="amount" />

                  <InputField
                    defaultValue={expense.amount}
                    pre={getCurrencySymbol(collective.currency)}
                    type="currency"
                    name="amount"
                    className="amountField"
                    onChange={amount => this.handleChange('amount', amount)}
                  />

                <FormattedMessage id="expense.incurredAt" defaultMessage="Date" />

                  <InputField
                    defaultValue={new Date()}
                    type="date"
                    name="incurredAt"
                    className="incurredAtField"
                    onChange={incurredAt => this.handleChange('incurredAt', incurredAt)}
                  />

                <FormattedMessage id="expense.category" defaultMessage="category" />

                  <InputField
                    type="select"
                    options={this.categoriesOptions}
                    defaultValue={expense.category}
                    name="category"
                    className="categoryField"
                    onChange={category => this.handleChange('category', category)}
                  />

                <FormattedMessage id="expense.payoutMethod" defaultMessage="payout method" />

              <InputField
                type="select"
                name="payoutMethod"
                options={payoutMethods}
                defaultValue={expense.payoutMethod}
                onChange={payoutMethod => this.handleChange('payoutMethod', payoutMethod)}
              />

            {this.state.expense.payoutMethod === 'paypal' && (

                  <FormattedMessage id="expense.payoutMethod.paypal.label" defaultMessage="PayPal address" />

                <InputField
                  type="email"
                  name="paypalEmail"
                  key={`paypalEmail-${get(LoggedInUser, 'id')}`}
                  defaultValue={this.state.expense.paypalEmail}
                  onChange={paypalEmail => this.handleChange('paypalEmail', paypalEmail)}
                />

            )}

                <FormattedMessage id="expense.privateMessage" defaultMessage="Private instructions" />

              <InputField
                type="textarea"
                name="privateMessage"
                onChange={privateMessage => this.handleChange('privateMessage', privateMessage)}
                defaultValue={expense.privateMessage}
                description={
                  <FormattedMessage
                    id="expense.privateMessage.description"
                    defaultMessage="Private instructions for the host to reimburse your expense"
                  />
                }
              />

                <Button className="blue" type="submit" disabled={this.state.loading || !this.state.isExpenseValid}>
                  {this.state.loading && <FormattedMessage id="form.processing" defaultMessage="processing" />}
                  {!this.state.loading && <FormattedMessage id="expense.new.submit" defaultMessage="Submit Expense" />}
                </Button>

              {this.state.error &&
{this.state.error}
}

        </form>

    );
  }

  render() {
    const { LoggedInUser, collective } = this.props;

    if (!LoggedInUser) {
      return (

            <FormattedMessage id="expenses.create.login" defaultMessage="Sign up or login to submit an expense." />

          <SignInForm next={`/${collective.slug}/expenses/new`} />

      );
    } else {
      return this.renderForm();
    }
  }
}

export default withIntl(CreateExpenseForm);
<commit_msg>fix(expenses/CreateExpenseForm): correct spacing around form<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage } from 'react-intl';
import { get } from 'lodash';
import Markdown from 'react-markdown';

import withIntl from '../../../lib/withIntl';
import { getCurrencySymbol } from '../../../lib/utils';

import InputField from '../../../components/InputField';
import SignInForm from '../../../components/SignInForm';
import categories from '../../../constants/categories';
import Button from '../../../components/Button';
import { P } from '../../../components/Text';
import Container from '../../../components/Container';

class CreateExpenseForm extends React.Component {
  static propTypes = {
    collective: PropTypes.object,
    LoggedInUser: PropTypes.object,
    onSubmit: PropTypes.func,
  };

  constructor(props) {
    super(props);
    this.getOptions = this.getOptions.bind(this);
    this.renderForm = this.renderForm.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);

    this.messages = defineMessages({
      paypal: {
        id: 'expense.payoutMethod.paypal',
        defaultMessage: 'PayPal ({paypalEmail, select, missing {missing} other {{paypalEmail}}})',
      },
      'newExpense.paypal.label': {
        id: 'newExpense.paypal.label',
        defaultMessage: 'Please provide address',
      },
      other: {
        id: 'expense.payoutMethod.manual',
        defaultMessage: 'Other (see instructions)',
      },
      donation: {
        id: 'expense.payoutMethod.donation',
        defaultMessage: 'Donation',
      },
      'error.descriptionMissing': {
        id: 'expense.error.descriptionMissing',
        defaultMessage: 'Missing description',
      },
      'error.amountMissing': {
        id: 'expense.error.amountMissing',
        defaultMessage: 'Amount must be greater than 0',
      },
      'error.privateMessageMissing': {
        id: 'expense.error.privateMessageMissing',
        defaultMessage: "Please provide instructions on how you'd like to be reimbursed as a private note",
      },
      'error.paypalEmailMissing': {
        id: 'expense.error.paypalEmailMissing',
        defaultMessage: 'Please provide your PayPal email address (or change the payout method)',
      },
      'error.attachmentMissing': {
        id: 'expense.error.attachmentMissing',
        defaultMessage: 'Missing attachment',
      },
    });

    this.categoriesOptions = categories(props.collective.slug).map(category => {
      return { [category]: category };
    });

    this.state = {
      modified: false,
      expense: {
        category: Object.keys(this.categoriesOptions[0])[0],
        payoutMethod: 'paypal',
        paypalEmail: (props.LoggedInUser && props.LoggedInUser.paypalEmail) || null,
      },
      isExpenseValid: false,
      loading: false,
    };
  }

  getOptions(arr, intlVars) {
    return arr.map(key => {
      const obj = {};
      obj[key] = this.props.intl.formatMessage(this.messages[key], intlVars);
      return obj;
    });
  }

  validate(expense) {
    const { intl } = this.props;
    if (!expense.description) {
      this.setState({
        error: intl.formatMessage(this.messages['error.descriptionMissing']),
      });
      return false;
    }
    if (!expense.amount > 0) {
      this.setState({
        error: intl.formatMessage(this.messages['error.amountMissing']),
      });
      return false;
    }
    if (!expense.attachment) {
      this.setState({
        error: intl.formatMessage(this.messages['error.attachmentMissing']),
      });
      return false;
    }
    if (expense.payoutMethod === 'other' && !expense.privateMessage) {
      this.setState({
        error: intl.formatMessage(this.messages['error.privateMessageMissing']),
      });
      return false;
    }
    if (expense.payoutMethod === 'paypal' && !expense.paypalEmail) {
      this.setState({
        error: intl.formatMessage(this.messages['error.paypalEmailMissing']),
      });
      return false;
    }
    this.setState({ error: null });
    return true;
  }

  handleChange(attr, value) {
    const expense = {
      ...this.state.expense,
      [attr]: value,
    };
    const newState = {
      modified: true,
      expense,
      isExpenseValid: this.validate(expense),
    };
    this.setState(newState);
    this.props.onChange && this.props.onChange(expense);
  }

  UNSAFE_componentWillReceiveProps(newProps) {
    if (!this.props.LoggedInUser && newProps.LoggedInUser && !this.state.expense.paypalEmail) {
      this.handleChange('paypalEmail', newProps.LoggedInUser.paypalEmail);
    }
  }

  async onSubmit(e) {
    if (e) {
      e.preventDefault();
    }
    try {
      await this.props.onSubmit(this.state.expense);
      this.setState({
        modified: false,
        isExpenseValid: false,
        expense: {},
        loading: false,
      });
    } catch (e) {
      this.setState({ loading: false });
      console.error('CreateExpenseForm onSubmit error', e);
    }
    return false;
  }

  renderForm() {
    const { LoggedInUser, intl, collective } = this.props;
    const { expense } = this.state;

    const payoutMethods = this.getOptions(['paypal', 'other', 'donation'], {
      paypalEmail: get(expense, 'user.paypalEmail') || intl.formatMessage(this.messages['newExpense.paypal.label']),
    });

    return (
      <div className={`CreateExpenseForm ${this.props.mode}`}>

        <Container as="form" onSubmit={this.onSubmit} maxWidth={[500, null, 800]} mx="auto">
          {!collective.expensePolicy && LoggedInUser && LoggedInUser.canEditCollective(collective) && (

                <FormattedMessage id="collective.expensePolicy.label" defaultMessage="Expense policy" />

                <FormattedMessage
                  id="collective.expensePolicy.description"
                  defaultMessage="It can be daunting for the community to file an expense. Help them by providing a clear expense policy to explain what they can expense."
                />

              <Button className="blue" href={`/${collective.slug}/edit/expenses`}>
                <FormattedMessage id="expense.expensePolicy.add" defaultMessage="add an expense policy" />
              </Button>

          )}
          {(collective.expensePolicy || get(collective, 'host.expensePolicy')) && (

                <FormattedMessage id="expense.expensePolicy" defaultMessage="Expense policy" />

              {collective.expensePolicy && <Markdown source={collective.expensePolicy} />}
              {get(collective, 'host.expensePolicy') && <Markdown source={get(collective, 'host.expensePolicy')} />}

          )}

            <FormattedMessage
              id="expense.disclaimer"
              defaultMessage="You must upload a valid receipt or invoice. We should be able to see clearly on the picture (or PDF) the total amount paid, the date, the items purchased and the legal address."
            />

              <FormattedMessage id="needHelp" defaultMessage="Need help?" />

              <InputField
                type="dropzone"
                options={{ accept: 'image/jpeg, image/png, application/pdf' }}
                name="attachment"
                className="attachmentField"
                onChange={attachment => this.handleChange('attachment', attachment)}
                defaultValue={expense.attachment}
                placeholder={'/static/images/receipt.svg'}
                description={
                  <FormattedMessage
                    id="expense.attachment.description"
                    defaultMessage="Upload receipt or invoice (photo or PDF)"
                  />
                }
              />

                  <FormattedMessage id="expense.description" defaultMessage="description" />

                    <InputField
                      type="text"
                      name="description"
                      defaultValue={expense.description}
                      className="descriptionField"
                      maxLength={255}
                      onChange={description => this.handleChange('description', description)}
                    />

                <FormattedMessage id="expense.amount" defaultMessage="amount" />

                  <InputField
                    defaultValue={expense.amount}
                    pre={getCurrencySymbol(collective.currency)}
                    type="currency"
                    name="amount"
                    className="amountField"
                    onChange={amount => this.handleChange('amount', amount)}
                  />

                <FormattedMessage id="expense.incurredAt" defaultMessage="Date" />

                  <InputField
                    defaultValue={new Date()}
                    type="date"
                    name="incurredAt"
                    className="incurredAtField"
                    onChange={incurredAt => this.handleChange('incurredAt', incurredAt)}
                  />

                <FormattedMessage id="expense.category" defaultMessage="category" />

                  <InputField
                    type="select"
                    options={this.categoriesOptions}
                    defaultValue={expense.category}
                    name="category"
                    className="categoryField"
                    onChange={category => this.handleChange('category', category)}
                  />

                <FormattedMessage id="expense.payoutMethod" defaultMessage="payout method" />

              <InputField
                type="select"
                name="payoutMethod"
                options={payoutMethods}
                defaultValue={expense.payoutMethod}
                onChange={payoutMethod => this.handleChange('payoutMethod', payoutMethod)}
              />

            {this.state.expense.payoutMethod === 'paypal' && (

                  <FormattedMessage id="expense.payoutMethod.paypal.label" defaultMessage="PayPal address" />

                <InputField
                  type="email"
                  name="paypalEmail"
                  key={`paypalEmail-${get(LoggedInUser, 'id')}`}
                  defaultValue={this.state.expense.paypalEmail}
                  onChange={paypalEmail => this.handleChange('paypalEmail', paypalEmail)}
                />

            )}

                <FormattedMessage id="expense.privateMessage" defaultMessage="Private instructions" />

              <InputField
                type="textarea"
                name="privateMessage"
                onChange={privateMessage => this.handleChange('privateMessage', privateMessage)}
                defaultValue={expense.privateMessage}
                description={
                  <FormattedMessage
                    id="expense.privateMessage.description"
                    defaultMessage="Private instructions for the host to reimburse your expense"
                  />
                }
              />

                <Button className="blue" type="submit" disabled={this.state.loading || !this.state.isExpenseValid}>
                  {this.state.loading && <FormattedMessage id="form.processing" defaultMessage="processing" />}
                  {!this.state.loading && <FormattedMessage id="expense.new.submit" defaultMessage="Submit Expense" />}
                </Button>

              {this.state.error &&
{this.state.error}
}

        </Container>

    );
  }

  render() {
    const { LoggedInUser, collective } = this.props;

    if (!LoggedInUser) {
      return (

            <FormattedMessage id="expenses.create.login" defaultMessage="Sign up or login to submit an expense." />

          <SignInForm next={`/${collective.slug}/expenses/new`} />

      );
    } else {
      return this.renderForm();
    }
  }
}

export default withIntl(CreateExpenseForm);
<|endoftext|>


<commit_before>const { get, upperFirst, camelCase } = require('lodash')

const metadata = require('../../../lib/metadata')
const { buildIncompleteFormList, toCompleteStageMessages } = require('../helpers')
const { isValidGuid } = require('../../../lib/controller-utils')
const { getDitCompany } = require('../../companies/repos')
const { getAdviser } = require('../../adviser/repos')
const { getInvestment } = require('../repos')

function getNextStage (currentStage, projectStages) {
  const projectStageIndex = projectStages.findIndex((projectStage) => {
    return projectStage.name.toLowerCase() === currentStage.toLowerCase()
  })
  return projectStages[projectStageIndex + 1]
}

function getCompanyDetails (req, res, next) {
  getDitCompany(req.session.token, req.params.companyId)
    .then((company) => {
      res.locals.company = company
      return next()
    })
    .catch(next)
}

async function getInvestmentDetails (req, res, next) {
  const investmentId = req.params.investmentId

  if (!isValidGuid(investmentId)) {
    return next()
  }
  try {
    const investmentProjectStages = metadata.investmentProjectStage
    const investmentData = await getInvestment(req.session.token, investmentId)
    const investorCompany = await getDitCompany(req.session.token, get(investmentData, 'investor_company.id'))
    const ukCompanyId = get(investmentData, 'uk_company.id')
    const clientRelationshipManagerId = get(investmentData, 'client_relationship_manager.id')
    const stageName = investmentData.stage.name

    investmentData.investor_company = Object.assign({}, investmentData.investor_company, investorCompany)

    if (ukCompanyId) {
      const companyDetails = await getDitCompany(req.session.token, ukCompanyId)
      investmentData.uk_company = Object.assign({}, investmentData.uk_company, companyDetails)
    }

    if (clientRelationshipManagerId) {
      const clientRelationshipManager = await getAdviser(req.session.token, clientRelationshipManagerId)
      investmentData.client_relationship_manager = clientRelationshipManager
    }

    res.locals.investmentData = investmentData
    res.locals.equityCompany = investmentData.investor_company
    res.locals.investmentProjectStages = investmentProjectStages.map((stage) => stage.name)

    res.locals.investmentStatus = {
      id: investmentData.id,
      meta: [
        {
          label: 'Status',
          value: upperFirst(investmentData.status),
          url: `/investment-projects/${investmentData.id}/status`,
          urlLabel: 'change',
        },
        {
          label: 'Project code',
          value: investmentData.project_code,
        },
        {
          label: 'Valuation',
          value: investmentData.value_complete ? 'Project valued' : 'Not yet valued',
        },
      ],
      company: {
        name: investmentData.investor_company.name,
        url: `/companies/${get(investmentData, 'investor_company.id')}`,
      },
      currentStage: {
        name: stageName,
        isComplete: investmentData.team_complete && investmentData.requirements_complete && investmentData.value_complete,
        incompleteFields: buildIncompleteFormList(get(investmentData, 'incomplete_fields', [])),
        messages: get(toCompleteStageMessages, camelCase(stageName), []),
      },
      nextStage: getNextStage(stageName, investmentProjectStages),
    }

    res.breadcrumb({
      name: investmentData.name,
      url: `/investment-projects/${investmentData.id}`,
    })

    next()
  } catch (error) {
    next(error)
  }
}

module.exports = {
  getCompanyDetails,
  getInvestmentDetails,
}
<commit_msg>Add created on to investment status meta<commit_after>const { get, upperFirst, camelCase } = require('lodash')
const format = require('date-fns/format')

const metadata = require('../../../lib/metadata')
const { buildIncompleteFormList, toCompleteStageMessages } = require('../helpers')
const { isValidGuid } = require('../../../lib/controller-utils')
const { getDitCompany } = require('../../companies/repos')
const { getAdviser } = require('../../adviser/repos')
const { getInvestment } = require('../repos')
const { mediumDateTimeFormat } = require('../../../../config')

function getNextStage (currentStage, projectStages) {
  const projectStageIndex = projectStages.findIndex((projectStage) => {
    return projectStage.name.toLowerCase() === currentStage.toLowerCase()
  })
  return projectStages[projectStageIndex + 1]
}

function getCompanyDetails (req, res, next) {
  getDitCompany(req.session.token, req.params.companyId)
    .then((company) => {
      res.locals.company = company
      return next()
    })
    .catch(next)
}

async function getInvestmentDetails (req, res, next) {
  const investmentId = req.params.investmentId

  if (!isValidGuid(investmentId)) {
    return next()
  }
  try {
    const investmentProjectStages = metadata.investmentProjectStage
    const investmentData = await getInvestment(req.session.token, investmentId)
    const investorCompany = await getDitCompany(req.session.token, get(investmentData, 'investor_company.id'))
    const ukCompanyId = get(investmentData, 'uk_company.id')
    const clientRelationshipManagerId = get(investmentData, 'client_relationship_manager.id')
    const stageName = investmentData.stage.name

    investmentData.investor_company = Object.assign({}, investmentData.investor_company, investorCompany)

    if (ukCompanyId) {
      const companyDetails = await getDitCompany(req.session.token, ukCompanyId)
      investmentData.uk_company = Object.assign({}, investmentData.uk_company, companyDetails)
    }

    if (clientRelationshipManagerId) {
      const clientRelationshipManager = await getAdviser(req.session.token, clientRelationshipManagerId)
      investmentData.client_relationship_manager = clientRelationshipManager
    }

    res.locals.investmentData = investmentData
    res.locals.equityCompany = investmentData.investor_company
    res.locals.investmentProjectStages = investmentProjectStages.map((stage) => stage.name)

    res.locals.investmentStatus = {
      id: investmentData.id,
      meta: [
        {
          label: 'Status',
          value: upperFirst(investmentData.status),
          url: `/investment-projects/${investmentData.id}/status`,
          urlLabel: 'change',
        },
        {
          label: 'Project code',
          value: investmentData.project_code,
        },
        {
          label: 'Valuation',
          value: investmentData.value_complete ? 'Project valued' : 'Not yet valued',
        },
        {
          label: 'Created on',
          value: format(investmentData.created_on, mediumDateTimeFormat),
        },
      ],
      company: {
        name: investmentData.investor_company.name,
        url: `/companies/${get(investmentData, 'investor_company.id')}`,
      },
      currentStage: {
        name: stageName,
        isComplete: investmentData.team_complete && investmentData.requirements_complete && investmentData.value_complete,
        incompleteFields: buildIncompleteFormList(get(investmentData, 'incomplete_fields', [])),
        messages: get(toCompleteStageMessages, camelCase(stageName), []),
      },
      nextStage: getNextStage(stageName, investmentProjectStages),
    }

    res.breadcrumb({
      name: investmentData.name,
      url: `/investment-projects/${investmentData.id}`,
    })

    next()
  } catch (error) {
    next(error)
  }
}

module.exports = {
  getCompanyDetails,
  getInvestmentDetails,
}
<|endoftext|>


<commit_before>/*
 *  (c) Daniel Arroyo. 3DaGoGo, Inc. (daniel@astroprint.com)
 *
 *  Distributed under the GNU Affero General Public License http://www.gnu.org/licenses/agpl.html
 */

window.AP_SESSION_ID = null;

var SocketData = Backbone.Model.extend({
  connectionView: null,
  _socket: null,
  _autoReconnectTrial: 0,
  _nextReconnectAttempt: null,
  _autoReconnectTimeouts: [1, 1, 2, 3, 5, 8, 13, 20, 40, 100],
  currentState: 0,
  loggedUser: LOGGED_USER, //username or null
  defaults: {
    box_reachable: 'unreachable', //unreachable, reachable, checking
    online: false,
    printing: false,
    paused: false,
    camera: false,
    printing_progress: {
      percent: 0.0,
      time_left: 0
    },
    temps: {},
    astroprint: {
      status: null
    },
    printer: {
      status: null
    },
    print_capture: null,
    tool: null,
    printing_speed: "100",
    printing_flow: "100"
  },
  extruder_count: null,
  initialize: function()
  {
    this.set('printing', initial_states.printing || initial_states.paused);
    this.set('paused', initial_states.paused);
    this.set('online', initial_states.online);
    this.set('print_capture', initial_states.print_capture);

    //See if we need to check for sofware version
    if (initial_states.checkSoftware && initial_states.online) {
      $.get(API_BASEURL + 'settings/software/check')
        .done(_.bind(function(data) {
          if (data.update_available) {
            this.trigger('new_sw_release');
          }
        }, this));
    }
  },
  connect: function(token)
  {
    this.set('box_reachable', 'checking');

    var options = {};
    if (SOCKJS_DEBUG) {
      options["debug"] = true;
    }

    this._socket = new SockJS(SOCKJS_URI+'?token='+token, undefined, options);
    this._socket.onopen = _.bind(this._onConnect, this);
    this._socket.onclose = _.bind(this._onClose, this);
    this._socket.onmessage = _.bind(this._onMessage, this);
  },
  reconnect: function() {
    if (this._socket) {
      this._socket.close();
      delete this._socket;
    }

    $.getJSON('/wsToken')
      .done(_.bind(function(data){
        if (data && data.ws_token) {
          this.connect(data.ws_token);
        }
      }, this))
      .fail(_.bind(function(){
        this._onClose();
      }, this));
  },
  _onConnect: function() {
    if (this._nextReconnectAttempt) {
      clearTimeout(this._nextReconnectAttempt);
      this._nextReconnectAttempt = null;
    }
    this._autoReconnectTrial = 0;
    this.set('box_reachable', 'reachable');
    //Get some initials
    if (this.extruder_count == null) {
      this.extruder_count = (app.printerProfile.toJSON())['extruder_count'];
    }
  },
  _onClose: function(e) {
    if (e && e.code == 1000) {
      // it was us calling close
      return;
    }

    this.set('box_reachable', 'unreachable');
    this.connectionView.setPrinterConnection('failed');
    this.connectionView.setAstroprintConnection('failed');
    this._currentState = 0;

    if (this._autoReconnectTrial < this._autoReconnectTimeouts.length) {
      var timeout = this._autoReconnectTimeouts[this._autoReconnectTrial];

      console.log("Reconnect trial #" + this._autoReconnectTrial + ", waiting " + timeout + "s");

      this._nextReconnectAttempt = setTimeout(_.bind(this.reconnect, this), timeout * 1000);
      this._autoReconnectTrial++;
    } else {
      this._onReconnectFailed();
    }
  },
  _onReconnectFailed: function() {
    console.error('reconnect failed');
  },
  _onMessage: function(e) {
    for (var prop in e.data) {
      var data = e.data[prop];

      switch (prop) {
        case "connected": {
          // update the current UI API key and send it with any request
          UI_API_KEY = data["apikey"];
          AP_SESSION_ID = data["sessionId"];
          $.ajaxSetup({
            headers: {"X-Api-Key": UI_API_KEY}
          });

          this.connectionView.connect();
        }
        break;

        case "current": {
          var flags = data.state.flags;
          if (data.temps.length) {
            var temps = data.temps[data.temps.length-1];
            var extruders = [];
            var tool = null;

            for (var i = 0; i < this.extruder_count; i++) {
              if (temps['tool' + i]) {
                tool = { "current": temps['tool' + i].actual, "target": temps['tool' + i].target }
                extruders[i] = tool ;
              }
            }

            this.set('temps', {
              bed: temps.bed,
              extruders: extruders
            });
          }

          if (data.state && data.state.text != this.currentState) {
            this.currentState = data.state.text;
            var connectionClass = 'blink-animation';
            var printerStatus = 'connecting';

            if (flags.closedOrError) {
              connectionClass = 'failed';
              printerStatus = 'failed';
            } else if (flags.operational) {
              connectionClass = 'connected';
              printerStatus = 'connected';
            }

            this.connectionView.setPrinterConnection(connectionClass);
            this.set('printer', {status: printerStatus });
          }

          if (!flags.paused) {
            this.set('printing', flags.printing);
          }

          this.set('paused', flags.paused);
          this.set('camera', flags.camera);

          if (flags.printing || flags.paused) {
            var progress = data.progress;
            var job = data.job;

            var printFileName = job.file.printFileName ? job.file.printFileName : job.file.name

            this.set('printing_progress', {
              filename: job.file.name,
              printFileName: printFileName,
              rendered_image: job.file.rendered_image,
              layer_count: job.layerCount,
              current_layer: progress.currentLayer,
              percent: progress.completion ? progress.completion.toFixed(1) : 0,
              time_left: data.progress.printTimeLeft,
              time_elapsed: progress.printTime ? progress.printTime : 0,
              heating_up: flags.heatingUp
            });
          }

          this.set('tool', data.tool);
          this.set('printing_speed', data.printing_speed);
          this.set('printing_flow', data.printing_flow);
        }
        break;

        case "event": {
          var type = data["type"];
          var payload = data["payload"];

          switch(type) {
            case 'MetadataAnalysisFinished':
              app.eventManager.trigger('astrobox:MetadataAnalysisFinished', payload);
            break;

            case 'CloudDownloadEvent':
              app.eventManager.trigger('astrobox:cloudDownloadEvent', payload);
            break;

            case 'CopyToHomeProgress':
              if(payload.observerId == AP_SESSION_ID){
                app.eventManager.trigger('astrobox:copyToHomeProgress', payload);
              }
            break;

            case 'ExternalDriveMounted':
              payload.action = 'mounted';
              app.eventManager.trigger('astrobox:externalDriveMounted', payload);
            break;

            case 'ExternalDriveEjected':
              payload.action = 'ejected';
              app.eventManager.trigger('astrobox:externalDriveEjected', payload);
            break;

            case 'ExternalDrivePhisicallyRemoved':
              payload.action = 'removed';
              app.eventManager.trigger('astrobox:externalDrivePhisicallyRemoved', payload);
            break;

            case 'AstroPrintStatus':
              switch(payload) {
                case 'connecting':
                  this.connectionView.setAstroprintConnection('blink-animation');
                break;

                case 'connected':
                  this.connectionView.setAstroprintConnection('connected');
                break;

                case 'disconnected':
                case 'error':
                  this.connectionView.setAstroprintConnection('failed');
                break;

                default:
                  console.log('astroprintStatus unkonwn event: '+payload);
              }
              this.set('astroprint', { status: payload });
            break;

            case 'LockStatusChanged':
              if (payload != this.loggedUser) {
                location.reload();
              }
            break;

            case 'PrintCaptureInfoChanged':
              this.set('print_capture', payload);
            break;

            case 'NetworkStatus':
              this.set('online', payload == 'online');
            break;

            case 'InternetConnectingStatus':
              app.eventManager.trigger('astrobox:InternetConnectingStatus', payload);
            break;

            case 'GstreamerEvent':
              app.eventManager.trigger('astrobox:videoStreamingEvent',payload);
            break;

            case 'SoftwareUpdateEvent':
              //We need to release so that the update screen shows up
              window.location.reload()
            break;

            case 'ToolChange':
              this.set('tool', data.tool);
              break;

            default:
              console.warn('Unkonwn event received: '+type);
          }
        }
        break;

        case 'commsData':
          app.eventManager.trigger('astrobox:commsData', {direction: data.direction, data: data.data.trim()});
        break;
      }
    }
  }
});
<commit_msg>better control of printer icon<commit_after>/*
 *  (c) Daniel Arroyo. 3DaGoGo, Inc. (daniel@astroprint.com)
 *
 *  Distributed under the GNU Affero General Public License http://www.gnu.org/licenses/agpl.html
 */

window.AP_SESSION_ID = null;

var SocketData = Backbone.Model.extend({
  connectionView: null,
  _socket: null,
  _autoReconnectTrial: 0,
  _nextReconnectAttempt: null,
  _autoReconnectTimeouts: [1, 1, 2, 3, 5, 8, 13, 20, 40, 100],
  currentState: 0,
  loggedUser: LOGGED_USER, //username or null
  defaults: {
    box_reachable: 'unreachable', //unreachable, reachable, checking
    online: false,
    printing: false,
    paused: false,
    camera: false,
    printing_progress: {
      percent: 0.0,
      time_left: 0
    },
    temps: {},
    astroprint: {
      status: null
    },
    printer: {
      status: null
    },
    print_capture: null,
    tool: null,
    printing_speed: "100",
    printing_flow: "100"
  },
  extruder_count: null,
  initialize: function()
  {
    this.set('printing', initial_states.printing || initial_states.paused);
    this.set('paused', initial_states.paused);
    this.set('online', initial_states.online);
    this.set('print_capture', initial_states.print_capture);

    //See if we need to check for sofware version
    if (initial_states.checkSoftware && initial_states.online) {
      $.get(API_BASEURL + 'settings/software/check')
        .done(_.bind(function(data) {
          if (data.update_available) {
            this.trigger('new_sw_release');
          }
        }, this));
    }
  },
  connect: function(token)
  {
    this.set('box_reachable', 'checking');

    var options = {};
    if (SOCKJS_DEBUG) {
      options["debug"] = true;
    }

    this._socket = new SockJS(SOCKJS_URI+'?token='+token, undefined, options);
    this._socket.onopen = _.bind(this._onConnect, this);
    this._socket.onclose = _.bind(this._onClose, this);
    this._socket.onmessage = _.bind(this._onMessage, this);
  },
  reconnect: function() {
    if (this._socket) {
      this._socket.close();
      delete this._socket;
    }

    $.getJSON('/wsToken')
      .done(_.bind(function(data){
        if (data && data.ws_token) {
          this.connect(data.ws_token);
        }
      }, this))
      .fail(_.bind(function(){
        this._onClose();
      }, this));
  },
  _onConnect: function() {
    if (this._nextReconnectAttempt) {
      clearTimeout(this._nextReconnectAttempt);
      this._nextReconnectAttempt = null;
    }
    this._autoReconnectTrial = 0;
    this.set('box_reachable', 'reachable');
    //Get some initials
    if (this.extruder_count == null) {
      this.extruder_count = (app.printerProfile.toJSON())['extruder_count'];
    }
  },
  _onClose: function(e) {
    if (e && e.code == 1000) {
      // it was us calling close
      return;
    }

    this.set('box_reachable', 'unreachable');
    this.connectionView.setPrinterConnection('failed');
    this.connectionView.setAstroprintConnection('failed');
    this._currentState = 0;

    if (this._autoReconnectTrial < this._autoReconnectTimeouts.length) {
      var timeout = this._autoReconnectTimeouts[this._autoReconnectTrial];

      console.log("Reconnect trial #" + this._autoReconnectTrial + ", waiting " + timeout + "s");

      this._nextReconnectAttempt = setTimeout(_.bind(this.reconnect, this), timeout * 1000);
      this._autoReconnectTrial++;
    } else {
      this._onReconnectFailed();
    }
  },
  _onReconnectFailed: function() {
    console.error('reconnect failed');
  },
  _onMessage: function(e) {
    for (var prop in e.data) {
      var data = e.data[prop];

      switch (prop) {
        case "connected": {
          // update the current UI API key and send it with any request
          UI_API_KEY = data["apikey"];
          AP_SESSION_ID = data["sessionId"];
          $.ajaxSetup({
            headers: {"X-Api-Key": UI_API_KEY}
          });

          this.connectionView.connect();
        }
        break;

        case "current": {
          var flags = data.state.flags;
          if (data.temps.length) {
            var temps = data.temps[data.temps.length-1];
            var extruders = [];
            var tool = null;

            for (var i = 0; i < this.extruder_count; i++) {
              if (temps['tool' + i]) {
                tool = { "current": temps['tool' + i].actual, "target": temps['tool' + i].target }
                extruders[i] = tool ;
              }
            }

            this.set('temps', {
              bed: temps.bed,
              extruders: extruders
            });
          }

          if (data.state && data.state.text != this.currentState) {
            this.currentState = data.state.text;
            connectionClass = '';
            printerStatus = '';

            if (flags.state == 4) { //4 -> STATE_CONNECTING
              connectionClass = 'blink-animation';
              printerStatus = 'connecting';
            } else if (flags.closedOrError) {
              connectionClass = 'failed';
              printerStatus = 'failed';
            } else if (flags.operational) {
              connectionClass = 'connected';
              printerStatus = 'connected';
            }

            this.connectionView.setPrinterConnection(connectionClass);
            this.set('printer', {status: printerStatus });
          }

          if (!flags.paused) {
            this.set('printing', flags.printing);
          }

          this.set('paused', flags.paused);
          this.set('camera', flags.camera);

          if (flags.printing || flags.paused) {
            var progress = data.progress;
            var job = data.job;

            var printFileName = job.file.printFileName ? job.file.printFileName : job.file.name

            this.set('printing_progress', {
              filename: job.file.name,
              printFileName: printFileName,
              rendered_image: job.file.rendered_image,
              layer_count: job.layerCount,
              current_layer: progress.currentLayer,
              percent: progress.completion ? progress.completion.toFixed(1) : 0,
              time_left: data.progress.printTimeLeft,
              time_elapsed: progress.printTime ? progress.printTime : 0,
              heating_up: flags.heatingUp
            });
          }

          this.set('tool', data.tool);
          this.set('printing_speed', data.printing_speed);
          this.set('printing_flow', data.printing_flow);
        }
        break;

        case "event": {
          var type = data["type"];
          var payload = data["payload"];

          switch(type) {
            case 'MetadataAnalysisFinished':
              app.eventManager.trigger('astrobox:MetadataAnalysisFinished', payload);
            break;

            case 'CloudDownloadEvent':
              app.eventManager.trigger('astrobox:cloudDownloadEvent', payload);
            break;

            case 'CopyToHomeProgress':
              if(payload.observerId == AP_SESSION_ID){
                app.eventManager.trigger('astrobox:copyToHomeProgress', payload);
              }
            break;

            case 'ExternalDriveMounted':
              payload.action = 'mounted';
              app.eventManager.trigger('astrobox:externalDriveMounted', payload);
            break;

            case 'ExternalDriveEjected':
              payload.action = 'ejected';
              app.eventManager.trigger('astrobox:externalDriveEjected', payload);
            break;

            case 'ExternalDrivePhisicallyRemoved':
              payload.action = 'removed';
              app.eventManager.trigger('astrobox:externalDrivePhisicallyRemoved', payload);
            break;

            case 'AstroPrintStatus':
              switch(payload) {
                case 'connecting':
                  this.connectionView.setAstroprintConnection('blink-animation');
                break;

                case 'connected':
                  this.connectionView.setAstroprintConnection('connected');
                break;

                case 'disconnected':
                case 'error':
                  this.connectionView.setAstroprintConnection('failed');
                break;

                default:
                  console.log('astroprintStatus unkonwn event: '+payload);
              }
              this.set('astroprint', { status: payload });
            break;

            case 'LockStatusChanged':
              if (payload != this.loggedUser) {
                location.reload();
              }
            break;

            case 'PrintCaptureInfoChanged':
              this.set('print_capture', payload);
            break;

            case 'NetworkStatus':
              this.set('online', payload == 'online');
            break;

            case 'InternetConnectingStatus':
              app.eventManager.trigger('astrobox:InternetConnectingStatus', payload);
            break;

            case 'GstreamerEvent':
              app.eventManager.trigger('astrobox:videoStreamingEvent',payload);
            break;

            case 'SoftwareUpdateEvent':
              //We need to release so that the update screen shows up
              window.location.reload()
            break;

            case 'ToolChange':
              this.set('tool', data.tool);
              break;

            default:
              console.warn('Unkonwn event received: '+type);
          }
        }
        break;

        case 'commsData':
          app.eventManager.trigger('astrobox:commsData', {direction: data.direction, data: data.data.trim()});
        break;
      }
    }
  }
});
<|endoftext|>


<commit_before>(function (Crafty, $) {
  'use strict';

  Crafty.scene('Load', function() {
    Crafty.background('#FFF');
    Crafty.e('2D, DOM, Text')
      .attr({
        w: Config.viewportWidth,
        h: 20,
        x: 0,
        y: Config.viewportHeight / 2
      })
      .text('Loading...')
      .css({'text-align': 'center'});

    Crafty.load([
      '/web/images/neusa.png',
      '/web/images/objects.png',
      '/web/images/village.jpg',
      '/web/images/village_bg.png'
    ], function () {
        // Once the images are loaded...

        // Define the individual sprites in the image
        // Each one (spr_tree, etc.) becomes a component
        // These components' names are prefixed with "spr_"
        //  to remind us that they simply cause the entity
        //  to be drawn with a certain sprite
        Crafty.sprite(50, '/web/images/objects.png', {
          spr_bucket:  [0, 0]
        });

        // Define the PC's sprite to be the first sprite in the third row of the
        //  animation sprite map
        Crafty.sprite(50, 100, '/web/images/neusa.png', {
          spr_player:  [0, 0],
        }, 0, 0);

        // Pointless setTimeout
        setTimeout(function () {
          Crafty.scene('Main');
        }, 2000);
      });

  });
})(Crafty, jQuery);

<commit_msg>images name were wrong<commit_after>(function (Crafty, $) {
  'use strict';

  Crafty.scene('Load', function() {
    Crafty.background('#FFF');
    Crafty.e('2D, DOM, Text')
      .attr({
        w: Config.viewportWidth,
        h: 20,
        x: 0,
        y: Config.viewportHeight / 2
      })
      .text('Loading...')
      .css({'text-align': 'center'});

    Crafty.load([
      '/web/images/neusa.png',
      '/web/images/objetos3.png',
      '/web/images/tiles sheet.jpg',
      '/web/images/village_bg.png'
    ], function () {
        // Once the images are loaded...

        // Define the individual sprites in the image
        // Each one (spr_tree, etc.) becomes a component
        // These components' names are prefixed with "spr_"
        //  to remind us that they simply cause the entity
        //  to be drawn with a certain sprite
        Crafty.sprite(50, '/web/images/objetos3.png', {
          spr_bucket:  [0, 0]
        });

        // Define the PC's sprite to be the first sprite in the third row of the
        //  animation sprite map
        Crafty.sprite(50, 100, '/web/images/neusa.png', {
          spr_player:  [0, 0],
        }, 0, 0);

        // Pointless setTimeout
        setTimeout(function () {
          Crafty.scene('Main');
        }, 2000);
      });

  });
})(Crafty, jQuery);

<|endoftext|>


<commit_before>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.browser.Asset.Scene');

goog.require('shapy.browser.Asset');
goog.require('shapy.editor.Object');

/**
 * Class encapsulating all information about a scene.
 *
 * @constructor
 * @extends {shapy.browser.Asset}
 *
 * @param {!shapy.browser.Service} shBrowser The browser service.
 * @param {string}                 id        ID of the scene.
 * @param {=Object}                opt_data  Data from the server.
 */
shapy.browser.Asset.Scene = function(shBrowser, id , opt_data) {
  shapy.browser.Asset.call(
      this,
      shBrowser,
      id,
      shapy.browser.Asset.Type.SCENE,
      opt_data);
  var data = opt_data || {};

  /**
   * ID of the scene.
   * @public {string} @const
   */
  this.id = id;

  /**
   * Name of the scene.
   * @public {string}
   */
  this.name = data['name'] || 'Untitled Scene';

  /**
   * List of users editing the scene.
   * @public {Array<string>}
   */
  this.users = data['users'] || [];

  /**
   * List of objects in the scene.
   * @public {!Object<string, shapy.Object>}
   */
  this.objects = {};

  /**
   * Next identifier.
   * @private {string}
   */
  this.nextID_ = 0;

  // Preview image.
  this.image = data['preview'] || '/img/scene.svg';
};
goog.inherits(shapy.browser.Asset.Scene, shapy.browser.Asset);

/**
 * Loads the asset data.
 *
 * @param {Object} data
 */
shapy.browser.Asset.Scene.prototype.load = function(data) {
  this.loaded = true;
};

/**
 * Generates a new object ID.
 *
 * @return {string} Unique Object ID.
 */
shapy.browser.Asset.Scene.prototype.getNextID = function() {
  var id = this.nextID_;
  this.nextID_++;
  return 'obj_' + id;
};

/**
 * Adds a new user to the list of people editing.
 *
 * @param {string} user
 */
shapy.browser.Asset.Scene.prototype.addUser = function(user) {
  goog.array.insert(this.users, user);
};

/**
 * Removes a user from the list of people editing.
 *
 * @param {string} user
 */
shapy.browser.Asset.Scene.prototype.removeUser = function(user) {
  goog.array.remove(this.users, user);
};

/**
 * Sets the list of users.
 *
 * @param {string} users
 */
shapy.browser.Asset.Scene.prototype.setUsers = function(users) {
  this.users = users;
};

/**
 * Picks an object intersected by a ray.
 *
 * @param {!goog.vec.Ray}         ray
 * @param {!shapy.editor.Mode}    mode
 *
 * @return {!shapy.editor.Editable}
 */
shapy.browser.Asset.Scene.prototype.pickRay = function(ray, mode) {
  // Find all the editable parts that intersect the ray.
  var hits = goog.array.map(goog.object.getValues(this.objects), function(obj) {
    return obj.pickRay(ray);
  });
  hits = goog.array.flatten(hits);

  // Find all allowed objects in the current mode.
  hits = goog.array.filter(hits, function(hit) {
    return mode[hit.item.type];
  });

  if (goog.array.isEmpty(hits)) {
    return null;
  }

  goog.array.sort(hits, function(a, b) {
    var da = goog.vec.Vec3.distance(ray.origin, a.point);
    var db = goog.vec.Vec3.distance(ray.origin, b.point);
    return da - db;
  }, this);

  return hits[0].item;
};

/**
 * Picks a group of objects intersection a frustum.
 *
 * @param {!Array<Object>}         frustum  Frostrum.
 * @param {!shapy.editor.Editable} selected Currently selected editable.
 * @param {!shapy.editor.Mode}     mode     Current selection mode.
 *
 * @return {!shapy.editor.Editable}
 */
shapy.browser.Asset.Scene.prototype.pickFrustum = function(
    frustum, selected, mode)
{
  var hits = goog.array.map(goog.object.getValues(this.objects), function(obj) {
    var ps = obj.pickFrustum(frustum);
    if (!goog.array.isEmpty(ps)) {
      ps = goog.array.concat(ps, obj);
    }
    return ps;
  });
  hits = goog.array.flatten(hits);

  // Find all allowed objects in the current mode.
  hits = goog.array.filter(hits, function(hit) {
    return mode[hit.type];
  });

  // Allow selecting multiple parts of the currently selected object only.
  if (selected && !mode[shapy.editor.Editable.Type.OBJECT]) {
    var owner = selected;
    if (selected.type == shapy.editor.Editable.Type.PARTS_GROUP) {
      owner = selected.getObject();
    }
    hits = goog.array.filter(hits, function(hit) {
      return hit.object == owner;
    });

    return goog.array.isEmpty(hits) ? null : new shapy.editor.PartsGroup(hits);
  }

  return goog.array.isEmpty(hits) ? null : new shapy.editor.ObjectGroup(hits);
};

/**
 * Creates a new object, adding it to the scene.
 *
 * @param {number} w
 * @param {number} h
 * @param {number} d
 *
 * @return {!shapy.editor.Object}
 */
shapy.browser.Asset.Scene.prototype.createCube = function(w, h, d) {
  var id = this.getNextID();
  var object = shapy.editor.Object.createCube(id, this, w, h, d);
  this.objects[id] = object;
  return object;
};

/**
 * Creates a new object, adding it to the scene.
 *
 * @param {number} r
 * @param {number} slices
 * @param {number} stacks
 *
 * @return {!shapy.editor.Object}
 */
shapy.browser.Asset.Scene.prototype.createSphere = function(r, slices, stacks) {
  var id = this.getNextID();
  var object = shapy.editor.Object.createSphere(id, this, r, slices, stacks);
  this.objects[id] = object;
  return object;
};
<commit_msg>Pick frustum does not create an object group for a single object.<commit_after>// This file is part of the Shapy project.
// Licensing information can be found in the LICENSE file.
// (C) 2015 The Shapy Team. All rights reserved.
goog.provide('shapy.browser.Asset.Scene');

goog.require('shapy.browser.Asset');
goog.require('shapy.editor.Object');

/**
 * Class encapsulating all information about a scene.
 *
 * @constructor
 * @extends {shapy.browser.Asset}
 *
 * @param {!shapy.browser.Service} shBrowser The browser service.
 * @param {string}                 id        ID of the scene.
 * @param {=Object}                opt_data  Data from the server.
 */
shapy.browser.Asset.Scene = function(shBrowser, id , opt_data) {
  shapy.browser.Asset.call(
      this,
      shBrowser,
      id,
      shapy.browser.Asset.Type.SCENE,
      opt_data);
  var data = opt_data || {};

  /**
   * ID of the scene.
   * @public {string} @const
   */
  this.id = id;

  /**
   * Name of the scene.
   * @public {string}
   */
  this.name = data['name'] || 'Untitled Scene';

  /**
   * List of users editing the scene.
   * @public {Array<string>}
   */
  this.users = data['users'] || [];

  /**
   * List of objects in the scene.
   * @public {!Object<string, shapy.Object>}
   */
  this.objects = {};

  /**
   * Next identifier.
   * @private {string}
   */
  this.nextID_ = 0;

  // Preview image.
  this.image = data['preview'] || '/img/scene.svg';
};
goog.inherits(shapy.browser.Asset.Scene, shapy.browser.Asset);

/**
 * Loads the asset data.
 *
 * @param {Object} data
 */
shapy.browser.Asset.Scene.prototype.load = function(data) {
  this.loaded = true;
};

/**
 * Generates a new object ID.
 *
 * @return {string} Unique Object ID.
 */
shapy.browser.Asset.Scene.prototype.getNextID = function() {
  var id = this.nextID_;
  this.nextID_++;
  return 'obj_' + id;
};

/**
 * Adds a new user to the list of people editing.
 *
 * @param {string} user
 */
shapy.browser.Asset.Scene.prototype.addUser = function(user) {
  goog.array.insert(this.users, user);
};

/**
 * Removes a user from the list of people editing.
 *
 * @param {string} user
 */
shapy.browser.Asset.Scene.prototype.removeUser = function(user) {
  goog.array.remove(this.users, user);
};

/**
 * Sets the list of users.
 *
 * @param {string} users
 */
shapy.browser.Asset.Scene.prototype.setUsers = function(users) {
  this.users = users;
};

/**
 * Picks an object intersected by a ray.
 *
 * @param {!goog.vec.Ray}         ray
 * @param {!shapy.editor.Mode}    mode
 *
 * @return {!shapy.editor.Editable}
 */
shapy.browser.Asset.Scene.prototype.pickRay = function(ray, mode) {
  // Find all the editable parts that intersect the ray.
  var hits = goog.array.map(goog.object.getValues(this.objects), function(obj) {
    return obj.pickRay(ray);
  });
  hits = goog.array.flatten(hits);

  // Find all allowed objects in the current mode.
  hits = goog.array.filter(hits, function(hit) {
    return mode[hit.item.type];
  });

  if (goog.array.isEmpty(hits)) {
    return null;
  }

  goog.array.sort(hits, function(a, b) {
    var da = goog.vec.Vec3.distance(ray.origin, a.point);
    var db = goog.vec.Vec3.distance(ray.origin, b.point);
    return da - db;
  }, this);

  return hits[0].item;
};

/**
 * Picks a group of objects intersection a frustum.
 *
 * @param {!Array<Object>}         frustum  Frostrum.
 * @param {!shapy.editor.Editable} selected Currently selected editable.
 * @param {!shapy.editor.Mode}     mode     Current selection mode.
 *
 * @return {!shapy.editor.Editable}
 */
shapy.browser.Asset.Scene.prototype.pickFrustum = function(
    frustum, selected, mode)
{
  var hits = goog.array.map(goog.object.getValues(this.objects), function(obj) {
    var ps = obj.pickFrustum(frustum);
    if (!goog.array.isEmpty(ps)) {
      ps = goog.array.concat(ps, obj);
    }
    return ps;
  });
  hits = goog.array.flatten(hits);

  // Find all allowed objects in the current mode.
  hits = goog.array.filter(hits, function(hit) {
    return mode[hit.type];
  });

  // Allow selecting multiple parts of the currently selected object only.
  if (selected && !mode[shapy.editor.Editable.Type.OBJECT]) {
    var owner = selected;
    if (selected.type == shapy.editor.Editable.Type.PARTS_GROUP) {
      owner = selected.getObject();
    }
    hits = goog.array.filter(hits, function(hit) {
      return hit.object == owner;
    });

    return goog.array.isEmpty(hits) ? null : new shapy.editor.PartsGroup(hits);
  }

  if (goog.array.isEmpty(hits)) {
    return null;
  }

  if (hits.length == 1) {
    return hits[0];
  } else {
    return new shapy.editor.ObjectGroup(hits);
  }
};

/**
 * Creates a new object, adding it to the scene.
 *
 * @param {number} w
 * @param {number} h
 * @param {number} d
 *
 * @return {!shapy.editor.Object}
 */
shapy.browser.Asset.Scene.prototype.createCube = function(w, h, d) {
  var id = this.getNextID();
  var object = shapy.editor.Object.createCube(id, this, w, h, d);
  this.objects[id] = object;
  return object;
};

/**
 * Creates a new object, adding it to the scene.
 *
 * @param {number} r
 * @param {number} slices
 * @param {number} stacks
 *
 * @return {!shapy.editor.Object}
 */
shapy.browser.Asset.Scene.prototype.createSphere = function(r, slices, stacks) {
  var id = this.getNextID();
  var object = shapy.editor.Object.createSphere(id, this, r, slices, stacks);
  this.objects[id] = object;
  return object;
};
<|endoftext|>


<commit_before>// analytics.js might not have loaded it's integrations by the time we start
// tracking events, page views and identifies.
// So we can use these *WhenReady() functions to cause the action to be
// deferred until all the intgrations are ready.
//
// TODO consider whether we should export something like this, maybe provide
// our own api instead of just using analytics.js' api
var trackEventWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.track.apply(this, _args);});
};

var trackPageWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.page.apply(this, _args);});
};

var identifyWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.identify.apply(this, _args);});
};

/*
* getUserEmail()
* Figure out the user's correct email address. This helps the differing keys
* in the database when using oAuth login.
*/
getUserEmail = function(){
  if (Meteor.userId()) {
    var user = AnalyticsUsers.findOne({_id: Meteor.userId()}, {
      fields: {
        emails: 1,
        "services.facebook.email": 1,
        "services.google.email": 1,
        "services.github.email": 1
      }
    });
    if ( user && user.emails ) {
      if (user.emails[0]) {
        return user.emails[0].address;
      } else {
        return null;
      }
    } else if ( user && user.services ) {
      var services = user.services;
      if ( services.facebook ) {
        return services.facebook.email;
      } else if ( services.github ) {
        return services.github.email;
      } else if ( services.google ) {
        return services.google.email;
      } else {
        return null;
      }
    }
  }
};

var initialized;
var trackLogins = function () {
  // don't run the first time, but we need to access Meteor.userId()
  // so that it's reactive
  Meteor.userId();
  if (initialized) {
    // when Meteor.userId() changes this will run
    if (Meteor.userId()) {
      // TODO I think it's not guaranteed that userEmail has been set because
      // the 'analyticsusers' publication might not be ready yet.
      identifyWhenReady(Meteor.userId(), {email: userEmail});
      trackEventWhenReady("Signed in");
    } else {
      trackEventWhenReady("Signed out");
    }
  }
  initialized = true;
};

var _IronRouter = (Package['iron:router'] && Package['iron:router'].Router);
var _FlowRouter = (Package['kadira:flow-router'] && Package['kadira:flow-router'].FlowRouter) ||
                  (Package['meteorhacks:flow-router'] && Package['meteorhacks:flow-router'].FlowRouter) ||
                  (Package['kadira:flow-router-ssr'] && Package['kadira:flow-router-ssr'].FlowRouter) ||
                  (Package['meteorhacks:flow-router-ssr'] && Package['meteorhacks:flow-router-ssr'].FlowRouter);

initIronRouter = function(){
  if (_IronRouter) {
    _IronRouter.onRun(function() {
      var router = this;
      Tracker.afterFlush(function () { trackPageWhenReady(router.route.getName()); });
      this.next();
    });
  }
}

initFlowRouter = function(){
  if (_FlowRouter) {
    // something context & context.context don't exist, see: #93
    _FlowRouter.triggers.enter([function(context){
      var page = {};

      if (context.path){
        page.path = context.path;
      }
      if (context.context && context.context.title){
        page.title = context.context.title;
      }

      page.url = window.location.origin + page.path;

      if (context.route && context.route.name) {
        page.name = context.route.name;
      } else {
        page.name = page.path;
      }
      if (context.context && context.context.querystring) {
        page.search = "?" + context.context.querystring;
      } else {
        page.search = "";
      }
      if (_FlowRouter.lastRoutePath) {
        page.referrer = window.location.origin + _FlowRouter.lastRoutePath;
      } else {
        page.referrer = document.referrer;
      }
      _FlowRouter.lastRoutePath = page.path;

      trackPageWhenReady(page.name, page);
    }]);
  }
}

var userEmail;
Meteor.startup(function () {
  if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.analyticsSettings) {
    var settings = Meteor.settings.public.analyticsSettings;
    if(settings.autorun !== false){
      initFlowRouter();
      initIronRouter();
    }
    analytics.initialize(settings);
  } else {
    console.log("Missing analyticsSettings in Meteor.settings.public");
  }

  if (Package['accounts-base']) {
    Tracker.autorun(function () {
      userEmail = getUserEmail();
    });
    Tracker.autorun(trackLogins);
  }
});

analytics = this.analytics;
try {
    delete this.analytics;
} catch (e) {
}
<commit_msg>remove useless try catch<commit_after>// analytics.js might not have loaded it's integrations by the time we start
// tracking events, page views and identifies.
// So we can use these *WhenReady() functions to cause the action to be
// deferred until all the intgrations are ready.
//
// TODO consider whether we should export something like this, maybe provide
// our own api instead of just using analytics.js' api
var trackEventWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.track.apply(this, _args);});
};

var trackPageWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.page.apply(this, _args);});
};

var identifyWhenReady = function () {
  var _args = arguments;
  analytics.ready(function () {analytics.identify.apply(this, _args);});
};

/*
* getUserEmail()
* Figure out the user's correct email address. This helps the differing keys
* in the database when using oAuth login.
*/
getUserEmail = function(){
  if (Meteor.userId()) {
    var user = AnalyticsUsers.findOne({_id: Meteor.userId()}, {
      fields: {
        emails: 1,
        "services.facebook.email": 1,
        "services.google.email": 1,
        "services.github.email": 1
      }
    });
    if ( user && user.emails ) {
      if (user.emails[0]) {
        return user.emails[0].address;
      } else {
        return null;
      }
    } else if ( user && user.services ) {
      var services = user.services;
      if ( services.facebook ) {
        return services.facebook.email;
      } else if ( services.github ) {
        return services.github.email;
      } else if ( services.google ) {
        return services.google.email;
      } else {
        return null;
      }
    }
  }
};

var initialized;
var trackLogins = function () {
  // don't run the first time, but we need to access Meteor.userId()
  // so that it's reactive
  Meteor.userId();
  if (initialized) {
    // when Meteor.userId() changes this will run
    if (Meteor.userId()) {
      // TODO I think it's not guaranteed that userEmail has been set because
      // the 'analyticsusers' publication might not be ready yet.
      identifyWhenReady(Meteor.userId(), {email: userEmail});
      trackEventWhenReady("Signed in");
    } else {
      trackEventWhenReady("Signed out");
    }
  }
  initialized = true;
};

var _IronRouter = (Package['iron:router'] && Package['iron:router'].Router);
var _FlowRouter = (Package['kadira:flow-router'] && Package['kadira:flow-router'].FlowRouter) ||
                  (Package['meteorhacks:flow-router'] && Package['meteorhacks:flow-router'].FlowRouter) ||
                  (Package['kadira:flow-router-ssr'] && Package['kadira:flow-router-ssr'].FlowRouter) ||
                  (Package['meteorhacks:flow-router-ssr'] && Package['meteorhacks:flow-router-ssr'].FlowRouter);

initIronRouter = function(){
  if (_IronRouter) {
    _IronRouter.onRun(function() {
      var router = this;
      Tracker.afterFlush(function () { trackPageWhenReady(router.route.getName()); });
      this.next();
    });
  }
}

initFlowRouter = function(){
  if (_FlowRouter) {
    // something context & context.context don't exist, see: #93
    _FlowRouter.triggers.enter([function(context){
      var page = {};

      if (context.path){
        page.path = context.path;
      }
      if (context.context && context.context.title){
        page.title = context.context.title;
      }

      page.url = window.location.origin + page.path;

      if (context.route && context.route.name) {
        page.name = context.route.name;
      } else {
        page.name = page.path;
      }
      if (context.context && context.context.querystring) {
        page.search = "?" + context.context.querystring;
      } else {
        page.search = "";
      }
      if (_FlowRouter.lastRoutePath) {
        page.referrer = window.location.origin + _FlowRouter.lastRoutePath;
      } else {
        page.referrer = document.referrer;
      }
      _FlowRouter.lastRoutePath = page.path;

      trackPageWhenReady(page.name, page);
    }]);
  }
}

var userEmail;
Meteor.startup(function () {
  if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.analyticsSettings) {
    var settings = Meteor.settings.public.analyticsSettings;
    if(settings.autorun !== false){
      initFlowRouter();
      initIronRouter();
    }
    analytics.initialize(settings);
  } else {
    console.log("Missing analyticsSettings in Meteor.settings.public");
  }

  if (Package['accounts-base']) {
    Tracker.autorun(function () {
      userEmail = getUserEmail();
    });
    Tracker.autorun(trackLogins);
  }
});

analytics = this.analytics;
delete this.analytics;
<|endoftext|>