<commit_before>/**
 * @fileoverview
 * Implementation of line based glyphs defined by a series of nodes and paths.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /core/kekule.structures.js
 * requires /chemdoc/kekule.glyph.base.js
 * requires /chemdoc/kekule.glyph.pathGlyphs.js
 */

(function(){
"use strict";

var NT = Kekule.Glyph.NodeType;
var PT = Kekule.Glyph.PathType;

/**
 * A glyph of straight line.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.StraightLine = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.StraightLine# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.StraightLine',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength
		var C = Kekule.CoordUtils;

		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var delta = {'x': refLength * (initialParams.lineLength || 1)};
		var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D);
		var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta));
		var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node1, node2]);
		this._applyParamsToConnector(connector, initialParams);
		this.appendNode(node1);
		this.appendNode(node2);
		this.appendConnector(connector);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		connector.setPathParams(initialParams);
	},

	/** @ignore */
	getAllowChildCoordStickTo: function(child, dest)
	{
		return true;
	},
	/** @ignore */
	getChildAcceptCoordStickFrom: function(child, fromObj)
	{
		return true;
	}
});

/**
 * A glyph of polygon formed by straight lines.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.Polygon = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.Polygon# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.Polygon',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	getRefLengthRatio: function()
	{
		return 1;
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength, edgeCount
		//   node props, connector props
		var C = Kekule.CoordUtils;
		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var nodeProps = initialParams.nodeProps;
		var connectorProps = initialParams.connectorProps;
		var r = refLength * (initialParams.lineLength || 1) * this.getRefLengthRatio();
		var edgeCount = initialParams.edgeCount || 3;
		var angleDelta = Math.PI * 2 / edgeCount;
		var startingAngle = (edgeCount % 2)?0: -angleDelta / 2;
		var currAngle = startingAngle;
		var firstNode, lastNode;
		for (var i = 0; i < edgeCount; ++i)
		{
			var c = {'x': r * Math.sin(currAngle), 'y': r * Math.cos(currAngle)};
			var node = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, c), C.add(coord3D, c));
			if (nodeProps)
				node.setPropValues(nodeProps);
			this.appendNode(node);
			if (i === 0)
				firstNode = node;
			if (lastNode)
			{
				var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [lastNode, node]);
				if (connectorProps)
					connector.setPropValues(connectorProps);
				this._applyParamsToConnector(connector, initialParams);
				this.appendConnector(connector);
			}
			lastNode = node;
			currAngle += angleDelta;
		}
		var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node, firstNode]);
		if (connectorProps)
			connector.setPropValues(connectorProps);
		this._applyParamsToConnector(connector, initialParams);
		this.appendConnector(connector);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		connector.setPathParams(initialParams);
	}
});

/**
 * A glyph of arc line.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.Arc = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.Arc# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.Arc',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength
		var C = Kekule.CoordUtils;

		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var delta = {'x': refLength * (initialParams.lineLength || 1)};
		var controllerDelta = {'x': 0, 'y': delta.x / 2};
		var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D);  // starting node
		var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta));  // ending node
		//var node3 = new Kekule.Glyph.PathGlyphNode(null, Kekule.Glyph.NodeType.CONTROLLER, C.add(coord2D, controllerDelta), C.add(coord3D, controllerDelta));  // control node
		var connector = new Kekule.Glyph.PathGlyphArcConnector(null, [node1, node2]);
		this._applyParamsToConnector(connector, initialParams);
		var controlPoint = connector.getControlPoint();
		//controlPoint.setDistanceToChord(delta.x)
		controlPoint.setCoord2D(controllerDelta)
				.setCoord3D(controllerDelta);

		this.appendNode(node1);
		this.appendNode(node2);
		this.appendConnector(connector);

		connector.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
		//node1.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
		//node2.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		connector.setPathParams(initialParams);
	}
});

})();<commit_msg>Enable autoOffset feature to straightline glyph<commit_after>/**
 * @fileoverview
 * Implementation of line based glyphs defined by a series of nodes and paths.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /core/kekule.structures.js
 * requires /chemdoc/kekule.glyph.base.js
 * requires /chemdoc/kekule.glyph.pathGlyphs.js
 */

(function(){
"use strict";

var NT = Kekule.Glyph.NodeType;
var PT = Kekule.Glyph.PathType;

/**
 * A glyph of straight line.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.StraightLine = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.StraightLine# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.StraightLine',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength
		var C = Kekule.CoordUtils;

		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var delta = {'x': refLength * (initialParams.lineLength || 1)};
		var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D);
		var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta));
		var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node1, node2]);
		this._applyParamsToConnector(connector, initialParams);
		this.appendNode(node1);
		this.appendNode(node2);
		this.appendConnector(connector);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		//connector.setPathParams(initialParams);

		var p = Object.create(initialParams);
		if (Kekule.ObjUtils.isUnset(initialParams.autoOffset))
			p.autoOffset = true;
		connector.setPathParams(p);
	},

	/** @ignore */
	getAllowChildCoordStickTo: function(child, dest)
	{
		return true;
	},
	/** @ignore */
	getChildAcceptCoordStickFrom: function(child, fromObj)
	{
		return true;
	}
});

/**
 * A glyph of polygon formed by straight lines.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.Polygon = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.Polygon# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.Polygon',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	getRefLengthRatio: function()
	{
		return 1;
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength, edgeCount
		//   node props, connector props
		var C = Kekule.CoordUtils;
		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var nodeProps = initialParams.nodeProps;
		var connectorProps = initialParams.connectorProps;
		var r = refLength * (initialParams.lineLength || 1) * this.getRefLengthRatio();
		var edgeCount = initialParams.edgeCount || 3;
		var angleDelta = Math.PI * 2 / edgeCount;
		var startingAngle = (edgeCount % 2)?0: -angleDelta / 2;
		var currAngle = startingAngle;
		var firstNode, lastNode;
		for (var i = 0; i < edgeCount; ++i)
		{
			var c = {'x': r * Math.sin(currAngle), 'y': r * Math.cos(currAngle)};
			var node = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, c), C.add(coord3D, c));
			if (nodeProps)
				node.setPropValues(nodeProps);
			this.appendNode(node);
			if (i === 0)
				firstNode = node;
			if (lastNode)
			{
				var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [lastNode, node]);
				if (connectorProps)
					connector.setPropValues(connectorProps);
				this._applyParamsToConnector(connector, initialParams);
				this.appendConnector(connector);
			}
			lastNode = node;
			currAngle += angleDelta;
		}
		var connector = new Kekule.Glyph.PathGlyphConnector(null, PT.LINE, [node, firstNode]);
		if (connectorProps)
			connector.setPropValues(connectorProps);
		this._applyParamsToConnector(connector, initialParams);
		this.appendConnector(connector);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		connector.setPathParams(initialParams);
	}
});

/**
 * A glyph of arc line.
 * @class
 * @augments Kekule.Glyph.PathGlyph
 */
Kekule.Glyph.Arc = Class.create(Kekule.Glyph.PathGlyph,
/** @lends Kekule.Glyph.Arc# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Glyph.Arc',
	/** @constructs */
	initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
	{
		$super(id, refLength, initialParams, coord2D, coord3D);
	},
	/** @private */
	doCreateDefaultStructure: function(refLength, initialParams)
	{
		// initialParams can include additional field: lineLength
		var C = Kekule.CoordUtils;

		var coord2D = {'x': 0, 'y': 0};
		var coord3D = {'x': 0, 'y': 0, 'z': 0};
		var delta = {'x': refLength * (initialParams.lineLength || 1)};
		var controllerDelta = {'x': 0, 'y': delta.x / 2};
		var node1 = new Kekule.Glyph.PathGlyphNode(null, null, coord2D, coord3D);  // starting node
		var node2 = new Kekule.Glyph.PathGlyphNode(null, null, C.add(coord2D, delta), C.add(coord3D, delta));  // ending node
		//var node3 = new Kekule.Glyph.PathGlyphNode(null, Kekule.Glyph.NodeType.CONTROLLER, C.add(coord2D, controllerDelta), C.add(coord3D, controllerDelta));  // control node
		var connector = new Kekule.Glyph.PathGlyphArcConnector(null, [node1, node2]);
		this._applyParamsToConnector(connector, initialParams);
		var controlPoint = connector.getControlPoint();
		//controlPoint.setDistanceToChord(delta.x)
		controlPoint.setCoord2D(controllerDelta)
				.setCoord3D(controllerDelta);

		this.appendNode(node1);
		this.appendNode(node2);
		this.appendConnector(connector);

		connector.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
		//node1.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
		//node2.setInteractMode(Kekule.ChemObjInteractMode.HIDDEN);
	},
	/** @private */
	_applyParamsToConnector: function(connector, initialParams)
	{
		connector.setPathParams(initialParams);
	}
});

})();<|endoftext|>


<commit_before>module.exports = {
  staging: {
    origin: process.env.ORIGIN || 'https://staging.jakduk.com',
    apiServerUrl: process.env.API_SERVER || '/api',
    internalApiServerUrl: process.env.INTERNAL_API_SERVER || 'https://staging.jakduk.com:8080/api',
    thumbnailServerUrl: process.env.THUMBNAIL_SERVER || 'https://staging.jakduk.com:8080',
    secureCookie: true,
    debug: true
  },
  prod: {
    origin: process.env.ORIGIN || 'https://jakduk.com',
    apiServerUrl: process.env.API_SERVER || '/api',
    internalApiServerUrl: process.env.INTERNAL_API_SERVER || 'https://api.jakduk.com/api',
    thumbnailServerUrl: process.env.THUMBNAIL_SERVER || 'https://api.jakduk.com',
    gaAccount: 'UA-59051176-1',
    secureCookie: true,
    debug: false
  }
};
<commit_msg>인증서 문제로 https -> http<commit_after>module.exports = {
  staging: {
    origin: process.env.ORIGIN || 'http://staging.jakduk.com',
    apiServerUrl: process.env.API_SERVER || '/api',
    internalApiServerUrl: process.env.INTERNAL_API_SERVER || 'http://staging.jakduk.com:8080/api',
    thumbnailServerUrl: process.env.THUMBNAIL_SERVER || 'http://staging.jakduk.com:8080',
    secureCookie: true,
    debug: true
  },
  prod: {
    origin: process.env.ORIGIN || 'http://jakduk.com',
    apiServerUrl: process.env.API_SERVER || '/api',
    internalApiServerUrl: process.env.INTERNAL_API_SERVER || 'http://api.jakduk.com/api',
    thumbnailServerUrl: process.env.THUMBNAIL_SERVER || 'http://api.jakduk.com',
    gaAccount: 'UA-59051176-1',
    secureCookie: true,
    debug: false
  }
};
<|endoftext|>


<commit_before>import models from '../../models';
import * as errors from '../errors';
import slugify from 'slug';
import { types } from '../../constants/collectives';
import roles from '../../constants/roles';
import activities from '../../constants/activities';

const defaultTiers = (collectiveData) => {
  const tiers = collectiveData.tiers || [];

  if (collectiveData.tags.indexOf("meetup") !== -1) {
    tiers.push({
      type: 'TIER',
      name: '1 month',
      description: "Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '1month-sponsor',
      amount: 25000,
      button: "become a sponsor"
    });
    tiers.push({
      type: 'TIER',
      name: '3 months',
      description: "**10% off!** - Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '3month-sponsor',
      amount: 67500,
      button: "become a sponsor"
    });
    tiers.push({
      type: 'TIER',
      name: '6 months',
      description: "**20% off!** - Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '6month-sponsor',
      amount: 120000,
      button: "become a sponsor"
    });
    return tiers;
  }
  if (collectiveData.tiers.length === 0) {
    tiers.push({
      type: 'TIER',
      name: 'backer',
      slug: 'backers',
      amount: 500,
      presets: [500, 1000, 2500, 5000],
      interval: 'month'
    });
    tiers.push({
      type: 'TIER',
      name: 'sponsor',
      slug: 'sponsors',
      amount: 10000,
      presets: [10000, 25000, 50000],
      interval: 'month'
    });
  }
  return tiers;
}

export function createCollective(_, args, req) {
  if (!req.remoteUser) {
    return Promise.reject(new errors.Unauthorized({ message: "You need to be logged in to create a collective"}));
  }

  if (!args.collective.name) {
    return Promise.reject(new errors.ValidationFailed({ message: "collective.name required" }));
  }

  let hostCollective, parentCollective, collective;

  const collectiveData = {
    ...args.collective,
    CreatedByUserId: req.remoteUser.id
  };

  collectiveData.tiers = collectiveData.tiers || [];
  if (collectiveData.tiers.length === 0) {
    collectiveData.tiers = defaultTiers(collectiveData);
  }

  const location = args.collective.location;
  if (location) {
    collectiveData.locationName = location.name;
    collectiveData.address = location.address;
    if (location.lat) {
      collectiveData.geoLocationLatLong = { type: 'Point', coordinates: [location.lat, location.long] };
    }
  }

  const promises = [];
  if (args.collective.HostCollectiveId) {
    if (args.collective.HostCollectiveId === 858) { // opencollective.com/meetups
      if (collectiveData.timezone) {
        if (collectiveData.timezone.match(/Europe/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 9807; // Open Collective Europe ASBL Host
        }
        if (collectiveData.timezone.match(/Paris/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 11284; // Open Collective Paris Host
        }
        if (collectiveData.timezone.match(/Brussels/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 9802; // BrusselsTogether ASBL Host
        }
        if (collectiveData.timezone.match(/London/)) {
          collectiveData.currency = 'GBP';
          args.collective.HostCollectiveId = 9806; // Open Collective UK Host
        }
      } else {
        args.collective.HostCollectiveId = 8674; // Open Collective Inc. Host
      }
      collectiveData.tags.push("Tech meetups");
    }
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.HostCollectiveId)
        .then(hc => {
          if (!hc) return Promise.reject(new Error(`Host collective with id ${args.collective.HostCollectiveId} not found`));
          hostCollective = hc;
          collectiveData.currency = collectiveData.currency || hc.currency;
          if (req.remoteUser.hasRole([roles.ADMIN, roles.HOST], hostCollective.id)) {
            collectiveData.isActive = true;
          }
        })
    );
  }
  if (args.collective.ParentCollectiveId) {
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.ParentCollectiveId)
        .then(pc => {
          if (!pc) return Promise.reject(new Error(`Parent collective with id ${args.collective.ParentCollectiveId} not found`));
          parentCollective = pc;
          // The currency of the new created collective if not specified should be the one of its direct parent or the host (in this order)
          collectiveData.currency = collectiveData.currency || pc.currency;
          if (req.remoteUser.hasRole([roles.ADMIN, roles.HOST, roles.MEMBER], parentCollective.id)) {
            collectiveData.isActive = true;
          }
        })
    );
  }
  return Promise.all(promises)
  .then(() => {
    // To ensure uniqueness of the slug, if the type of collective is not COLLECTIVE (e.g. EVENT)
    // we force the slug to be of the form of `${slug}-${ParentCollectiveId}${collective.type.substr(0,2)}`
    const slug = slugify(args.collective.slug || args.collective.name);
    if (collectiveData.ParentCollectiveId) {
      collectiveData.slug = `${slug}-${parentCollective.id}${collectiveData.type.substr(0,2)}`.toLowerCase();
      const canCreateEvent = req.remoteUser.hasRole(['ADMIN', 'HOST', 'BACKER'], parentCollective.id);
      if (!canCreateEvent) {
        return Promise.reject(new errors.Unauthorized({ message: `You must be logged in as a member of the ${parentCollective.slug} collective to create an event`}));
      }
    } else if (collectiveData.HostCollectiveData) {
      if (!hostCollective.settings.apply) {
        return Promise.reject(new errors.Unauthorized({ message: `This host does not accept applications for new collectives` }))
      } else {
        collectiveData.isActive = false;
      }
    }
  })
  .then(() => models.Collective.create(collectiveData))
  .then(c => collective = c)
  .then(() => collective.editTiers(collectiveData.tiers))
  .then(() => collective.addUserWithRole(req.remoteUser, roles.ADMIN, { CreatedByUserId: req.remoteUser.id }))
  .then(() => {
    if (collective.HostCollectiveId) {
      collective.addHost(hostCollective, req.remoteUser);
    }
  })
  .then(() => collective.editPaymentMethods(args.collective.paymentMethods, { CreatedByUserId: req.remoteUser.id }))
  .then(async () => {
    // if the type of collective is an organization or an event, we don't notify the host
    if (collective.type !== types.COLLECTIVE) {
      return collective;
    }
    const remoteUserCollective = await models.Collective.findById(req.remoteUser.CollectiveId);
    models.Activity.create({
      type: activities.COLLECTIVE_CREATED,
      UserId: req.remoteUser.id,
      CollectiveId: hostCollective.id,
      data: {
        collective: collective.info,
        host: hostCollective.info,
        user: {
          email: req.remoteUser.email,
          collective: remoteUserCollective.info
        }
      }
    })
    return collective;
  })
  .catch(e => {
    let msg;
    switch (e.name) {
      case "SequelizeUniqueConstraintError":
        msg = `The slug ${e.fields.slug.replace(/\-[0-9]+ev$/, '')} is already taken. Please use another name for your ${collectiveData.type.toLowerCase()}.`;
        break;
      default:
        msg = e.message;
        break;
    }
    throw new Error(msg);
  })
}

export function editCollective(_, args, req) {
  if (!req.remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to edit a collective" });
  }

  if (!args.collective.id) {
    return Promise.reject(new errors.ValidationFailed({ message: "collective.id required" }));
  }

  const location = args.collective.location || {};

  const updatedCollectiveData = {
    ...args.collective,
    locationName: location.name,
    address: location.address,
    LastEditedByUserId: req.remoteUser.id
  };

  updatedCollectiveData.type = updatedCollectiveData.type || 'COLLECTIVE';

  if (location.lat) {
    updatedCollectiveData.geoLocationLatLong = {
      type: 'Point',
      coordinates: [ location.lat, location.long ]
    };
  }

  let collective, parentCollective;

  const promises = [
    req.loaders.collective.findById.load(args.collective.id)
      .then(c => {
        if (!c) throw new Error(`Collective with id ${args.collective.id} not found`);
        collective = c;
      })
    ];

  if (args.collective.ParentCollectiveId) {
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.ParentCollectiveId)
        .then(pc => {
          if (!pc) return Promise.reject(new Error(`Parent collective with id ${args.collective.ParentCollectiveId} not found`));
          parentCollective = pc;
        })
    );
  }
  return Promise.all(promises)
  .then(() => {
    if (args.collective.slug && updatedCollectiveData.type === 'EVENT') {
      // To ensure uniqueness of the slug, if the type of collective is not COLLECTIVE (e.g. EVENT)
      // we force the slug to be of the form of `${slug}-${ParentCollectiveId}${collective.type.substr(0,2)}`
      const slug = slugify(args.collective.slug.replace(/(\-[0-9]+[a-z]{2})$/i, '') || args.collective.name);
      updatedCollectiveData.slug = `${slug}-${parentCollective.id}${collective.type.substr(0,2)}`.toLowerCase();
    }
    if (updatedCollectiveData.type === 'EVENT') {
      return (req.remoteUser.id === collective.CreatedByUserId) || req.remoteUser.hasRole(['ADMIN', 'HOST', 'BACKER'], parentCollective.id)
    } else {
      return (req.remoteUser.id === collective.CreatedByUserId) || req.remoteUser.hasRole(['ADMIN', 'HOST'], updatedCollectiveData.id)
    }
  })
  .then(canEditCollective => {
    if (!canEditCollective) {
      let errorMsg;
      switch (updatedCollectiveData.type) {
        case types.EVENT:
          errorMsg = `You must be logged in as the creator of this Event or as an admin of the ${parentCollective.slug} collective to edit this Event Collective`;
          break;

        case types.USER:
          errorMsg = `You must be logged in as ${updatedCollectiveData.name} to edit this User Collective`;
          break;

        default:
          errorMsg = `You must be logged in as an admin or as the host of this ${updatedCollectiveData.type.toLowerCase()} collective to edit it`;
      }
      return Promise.reject(new errors.Unauthorized({ message: errorMsg }));
    }
  })
  .then(() => collective.update(updatedCollectiveData))
  .then(() => collective.editTiers(args.collective.tiers))
  .then(() => collective.editMembers(args.collective.members, { CreatedByUserId: req.remoteUser.id }))
  .then(() => collective.editPaymentMethods(args.collective.paymentMethods, { CreatedByUserId: req.remoteUser.id }))
  .then(() => collective);
}

export async function approveCollective(remoteUser, CollectiveId) {
  if (!remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to approve a collective" });
  }

  const collective = await models.Collective.findById(CollectiveId);
  if (!collective) {
    throw new errors.NotFound({ message: `Collective with id ${CollectiveId} not found` });
  }

  const HostCollectiveId = await collective.getHostCollectiveId();
  if (!remoteUser.isAdmin(HostCollectiveId)) {
    throw new errors.Unauthorized({ message: "You need to be logged in as an admin of the host of this collective to approve it", data: { HostCollectiveId } });
  }

  return collective.update({ isActive: true });
}

export function deleteCollective(_, args, req) {
  if (!req.remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to delete a collective" });
  }

  return models.Collective.findById(args.id)
    .then(collective => {
      if (!collective) throw new errors.NotFound({ message: `Collective with id ${args.id} not found` });
      if (!req.remoteUser.isAdmin(collective.id) && !req.remoteUser.isAdmin(collective.ParentCollectiveId)) {
        throw new errors.Unauthorized({ message: "You need to be logged in as a core contributor or as a host to delete this collective" });
      }

      return collective.destroy();
    });
}<commit_msg>fix tests<commit_after>import models from '../../models';
import * as errors from '../errors';
import slugify from 'slug';
import { types } from '../../constants/collectives';
import roles from '../../constants/roles';
import activities from '../../constants/activities';

const defaultTiers = (collectiveData) => {
  const tiers = collectiveData.tiers || [];

  if (collectiveData.HostCollectiveId === 858) { // if request coming from opencollective.com/meetups
    tiers.push({
      type: 'TIER',
      name: '1 month',
      description: "Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '1month-sponsor',
      amount: 25000,
      button: "become a sponsor"
    });
    tiers.push({
      type: 'TIER',
      name: '3 months',
      description: "**10% off!** - Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '3month-sponsor',
      amount: 67500,
      button: "become a sponsor"
    });
    tiers.push({
      type: 'TIER',
      name: '6 months',
      description: "**20% off!** - Sponsor our meetup and get: a shout-out on social media, presence on the merch table and your logo on our meetup page.",
      slug: '6month-sponsor',
      amount: 120000,
      button: "become a sponsor"
    });
    return tiers;
  }
  if (collectiveData.tiers.length === 0) {
    tiers.push({
      type: 'TIER',
      name: 'backer',
      slug: 'backers',
      amount: 500,
      presets: [500, 1000, 2500, 5000],
      interval: 'month'
    });
    tiers.push({
      type: 'TIER',
      name: 'sponsor',
      slug: 'sponsors',
      amount: 10000,
      presets: [10000, 25000, 50000],
      interval: 'month'
    });
  }
  return tiers;
}

export function createCollective(_, args, req) {
  if (!req.remoteUser) {
    return Promise.reject(new errors.Unauthorized({ message: "You need to be logged in to create a collective"}));
  }

  if (!args.collective.name) {
    return Promise.reject(new errors.ValidationFailed({ message: "collective.name required" }));
  }

  let hostCollective, parentCollective, collective;

  const collectiveData = {
    ...args.collective,
    CreatedByUserId: req.remoteUser.id
  };

  collectiveData.tiers = collectiveData.tiers || [];
  if (collectiveData.tiers.length === 0) {
    collectiveData.tiers = defaultTiers(collectiveData);
  }

  const location = args.collective.location;
  if (location) {
    collectiveData.locationName = location.name;
    collectiveData.address = location.address;
    if (location.lat) {
      collectiveData.geoLocationLatLong = { type: 'Point', coordinates: [location.lat, location.long] };
    }
  }

  const promises = [];
  if (args.collective.HostCollectiveId) {
    if (args.collective.HostCollectiveId === 858) { // opencollective.com/meetups
      if (collectiveData.timezone) {
        if (collectiveData.timezone.match(/Europe/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 9807; // Open Collective Europe ASBL Host
        }
        if (collectiveData.timezone.match(/Paris/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 11284; // Open Collective Paris Host
        }
        if (collectiveData.timezone.match(/Brussels/)) {
          collectiveData.currency = 'EUR';
          args.collective.HostCollectiveId = 9802; // BrusselsTogether ASBL Host
        }
        if (collectiveData.timezone.match(/London/)) {
          collectiveData.currency = 'GBP';
          args.collective.HostCollectiveId = 9806; // Open Collective UK Host
        }
      } else {
        args.collective.HostCollectiveId = 8674; // Open Collective Inc. Host
      }
      collectiveData.tags.push("Tech meetups");
    }
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.HostCollectiveId)
        .then(hc => {
          if (!hc) return Promise.reject(new Error(`Host collective with id ${args.collective.HostCollectiveId} not found`));
          hostCollective = hc;
          collectiveData.currency = collectiveData.currency || hc.currency;
          if (req.remoteUser.hasRole([roles.ADMIN, roles.HOST], hostCollective.id)) {
            collectiveData.isActive = true;
          }
        })
    );
  }
  if (args.collective.ParentCollectiveId) {
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.ParentCollectiveId)
        .then(pc => {
          if (!pc) return Promise.reject(new Error(`Parent collective with id ${args.collective.ParentCollectiveId} not found`));
          parentCollective = pc;
          // The currency of the new created collective if not specified should be the one of its direct parent or the host (in this order)
          collectiveData.currency = collectiveData.currency || pc.currency;
          if (req.remoteUser.hasRole([roles.ADMIN, roles.HOST, roles.MEMBER], parentCollective.id)) {
            collectiveData.isActive = true;
          }
        })
    );
  }
  return Promise.all(promises)
  .then(() => {
    // To ensure uniqueness of the slug, if the type of collective is not COLLECTIVE (e.g. EVENT)
    // we force the slug to be of the form of `${slug}-${ParentCollectiveId}${collective.type.substr(0,2)}`
    const slug = slugify(args.collective.slug || args.collective.name);
    if (collectiveData.ParentCollectiveId) {
      collectiveData.slug = `${slug}-${parentCollective.id}${collectiveData.type.substr(0,2)}`.toLowerCase();
      const canCreateEvent = req.remoteUser.hasRole(['ADMIN', 'HOST', 'BACKER'], parentCollective.id);
      if (!canCreateEvent) {
        return Promise.reject(new errors.Unauthorized({ message: `You must be logged in as a member of the ${parentCollective.slug} collective to create an event`}));
      }
    } else if (collectiveData.HostCollectiveData) {
      if (!hostCollective.settings.apply) {
        return Promise.reject(new errors.Unauthorized({ message: `This host does not accept applications for new collectives` }))
      } else {
        collectiveData.isActive = false;
      }
    }
  })
  .then(() => models.Collective.create(collectiveData))
  .then(c => collective = c)
  .then(() => collective.editTiers(collectiveData.tiers))
  .then(() => collective.addUserWithRole(req.remoteUser, roles.ADMIN, { CreatedByUserId: req.remoteUser.id }))
  .then(() => {
    if (collective.HostCollectiveId) {
      collective.addHost(hostCollective, req.remoteUser);
    }
  })
  .then(() => collective.editPaymentMethods(args.collective.paymentMethods, { CreatedByUserId: req.remoteUser.id }))
  .then(async () => {
    // if the type of collective is an organization or an event, we don't notify the host
    if (collective.type !== types.COLLECTIVE) {
      return collective;
    }
    const remoteUserCollective = await models.Collective.findById(req.remoteUser.CollectiveId);
    models.Activity.create({
      type: activities.COLLECTIVE_CREATED,
      UserId: req.remoteUser.id,
      CollectiveId: hostCollective.id,
      data: {
        collective: collective.info,
        host: hostCollective.info,
        user: {
          email: req.remoteUser.email,
          collective: remoteUserCollective.info
        }
      }
    })
    return collective;
  })
  .catch(e => {
    let msg;
    switch (e.name) {
      case "SequelizeUniqueConstraintError":
        msg = `The slug ${e.fields.slug.replace(/\-[0-9]+ev$/, '')} is already taken. Please use another name for your ${collectiveData.type.toLowerCase()}.`;
        break;
      default:
        msg = e.message;
        break;
    }
    throw new Error(msg);
  })
}

export function editCollective(_, args, req) {
  if (!req.remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to edit a collective" });
  }

  if (!args.collective.id) {
    return Promise.reject(new errors.ValidationFailed({ message: "collective.id required" }));
  }

  const location = args.collective.location || {};

  const updatedCollectiveData = {
    ...args.collective,
    locationName: location.name,
    address: location.address,
    LastEditedByUserId: req.remoteUser.id
  };

  updatedCollectiveData.type = updatedCollectiveData.type || 'COLLECTIVE';

  if (location.lat) {
    updatedCollectiveData.geoLocationLatLong = {
      type: 'Point',
      coordinates: [ location.lat, location.long ]
    };
  }

  let collective, parentCollective;

  const promises = [
    req.loaders.collective.findById.load(args.collective.id)
      .then(c => {
        if (!c) throw new Error(`Collective with id ${args.collective.id} not found`);
        collective = c;
      })
    ];

  if (args.collective.ParentCollectiveId) {
    promises.push(
      req.loaders
        .collective.findById.load(args.collective.ParentCollectiveId)
        .then(pc => {
          if (!pc) return Promise.reject(new Error(`Parent collective with id ${args.collective.ParentCollectiveId} not found`));
          parentCollective = pc;
        })
    );
  }
  return Promise.all(promises)
  .then(() => {
    if (args.collective.slug && updatedCollectiveData.type === 'EVENT') {
      // To ensure uniqueness of the slug, if the type of collective is not COLLECTIVE (e.g. EVENT)
      // we force the slug to be of the form of `${slug}-${ParentCollectiveId}${collective.type.substr(0,2)}`
      const slug = slugify(args.collective.slug.replace(/(\-[0-9]+[a-z]{2})$/i, '') || args.collective.name);
      updatedCollectiveData.slug = `${slug}-${parentCollective.id}${collective.type.substr(0,2)}`.toLowerCase();
    }
    if (updatedCollectiveData.type === 'EVENT') {
      return (req.remoteUser.id === collective.CreatedByUserId) || req.remoteUser.hasRole(['ADMIN', 'HOST', 'BACKER'], parentCollective.id)
    } else {
      return (req.remoteUser.id === collective.CreatedByUserId) || req.remoteUser.hasRole(['ADMIN', 'HOST'], updatedCollectiveData.id)
    }
  })
  .then(canEditCollective => {
    if (!canEditCollective) {
      let errorMsg;
      switch (updatedCollectiveData.type) {
        case types.EVENT:
          errorMsg = `You must be logged in as the creator of this Event or as an admin of the ${parentCollective.slug} collective to edit this Event Collective`;
          break;

        case types.USER:
          errorMsg = `You must be logged in as ${updatedCollectiveData.name} to edit this User Collective`;
          break;

        default:
          errorMsg = `You must be logged in as an admin or as the host of this ${updatedCollectiveData.type.toLowerCase()} collective to edit it`;
      }
      return Promise.reject(new errors.Unauthorized({ message: errorMsg }));
    }
  })
  .then(() => collective.update(updatedCollectiveData))
  .then(() => collective.editTiers(args.collective.tiers))
  .then(() => collective.editMembers(args.collective.members, { CreatedByUserId: req.remoteUser.id }))
  .then(() => collective.editPaymentMethods(args.collective.paymentMethods, { CreatedByUserId: req.remoteUser.id }))
  .then(() => collective);
}

export async function approveCollective(remoteUser, CollectiveId) {
  if (!remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to approve a collective" });
  }

  const collective = await models.Collective.findById(CollectiveId);
  if (!collective) {
    throw new errors.NotFound({ message: `Collective with id ${CollectiveId} not found` });
  }

  const HostCollectiveId = await collective.getHostCollectiveId();
  if (!remoteUser.isAdmin(HostCollectiveId)) {
    throw new errors.Unauthorized({ message: "You need to be logged in as an admin of the host of this collective to approve it", data: { HostCollectiveId } });
  }

  return collective.update({ isActive: true });
}

export function deleteCollective(_, args, req) {
  if (!req.remoteUser) {
    throw new errors.Unauthorized({ message: "You need to be logged in to delete a collective" });
  }

  return models.Collective.findById(args.id)
    .then(collective => {
      if (!collective) throw new errors.NotFound({ message: `Collective with id ${args.id} not found` });
      if (!req.remoteUser.isAdmin(collective.id) && !req.remoteUser.isAdmin(collective.ParentCollectiveId)) {
        throw new errors.Unauthorized({ message: "You need to be logged in as a core contributor or as a host to delete this collective" });
      }

      return collective.destroy();
    });
}<|endoftext|>


<commit_before>import { get } from 'lodash';
import errors from '../../../lib/errors';
import roles from '../../../constants/roles';
import statuses from '../../../constants/expense_status';
import activities from '../../../constants/activities';
import models from '../../../models';
import paymentProviders from '../../../paymentProviders';
import { formatCurrency } from '../../../lib/utils';
import {
  createFromPaidExpense as createTransactionFromPaidExpense,
  createTransactionFromInKindDonation,
} from '../../../lib/transactions';
import { getFxRate } from '../../../lib/currency';
import debugLib from 'debug';

const debug = debugLib('expenses');

/**
 * Only admin of expense.collective or of expense.collective.host can approve/reject expenses
 */
function canUpdateExpenseStatus(remoteUser, expense) {
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.CollectiveId)) {
    return true;
  }
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.collective.HostCollectiveId)) {
    return true;
  }
  return false;
}

/**
 * Only admin of expense.collective.host can mark expenses unpaid
 */
function canMarkExpenseUnpaid(remoteUser, expense) {
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.collective.HostCollectiveId)) {
    return true;
  }
  return false;
}

/**
 * Only the author or an admin of the collective or collective.host can edit an expense when it hasn't been paid yet
 */
function canEditExpense(remoteUser, expense) {
  if (expense.status === statuses.PAID) return false;
  if (remoteUser.id === expense.UserId) {
    return true;
  }
  return canUpdateExpenseStatus(remoteUser, expense);
}

export async function updateExpenseStatus(remoteUser, expenseId, status) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to update the status of an expense');
  }

  if (Object.keys(statuses).indexOf(status) === -1) {
    throw new errors.ValidationFailed('Invalid status, status must be one of ', Object.keys(statuses).join(', '));
  }

  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canUpdateExpenseStatus(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to approve this expense");
  }
  switch (status) {
    case statuses.APPROVED:
      if (expense.status === statuses.PAID) {
        throw new errors.Unauthorized("You can't reject an expense that is already paid");
      }
      break;
    case statuses.REJECTED:
      if (expense.status === statuses.PAID) {
        throw new errors.Unauthorized("You can't approve an expense that is already paid");
      }
      break;
    case statuses.PAID:
      if (expense.status !== statuses.APPROVED) {
        throw new errors.Unauthorized('The expense must be approved before you can set it to paid');
      }
      break;
  }
  const res = await expense.update({ status, lastEditedById: remoteUser.id });
  return res;
}

export async function createExpense(remoteUser, expenseData) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to create an expense');
  }
  if (!get(expenseData, 'collective.id')) {
    throw new errors.Unauthorized('Missing expense.collective.id');
  }

  // Update remoteUser's paypal email if it has changed
  if (get(expenseData, 'user.paypalEmail') !== remoteUser.paypalEmail) {
    remoteUser.paypalEmail = get(expenseData, 'user.paypalEmail');
    remoteUser.save();
  }
  expenseData.UserId = remoteUser.id;

  const collective = await models.Collective.findByPk(expenseData.collective.id);

  if (expenseData.currency && expenseData.currency !== collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${expenseData.collective.currency})`,
    );
  }

  if (!collective) {
    throw new errors.ValidationFailed('Collective not found');
  }

  if (expenseData.currency && expenseData.currency !== collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${collective.currency})`,
    );
  }

  const expense = await models.Expense.create({
    ...expenseData,
    status: statuses.PENDING,
    CollectiveId: collective.id,
    lastEditedById: expenseData.UserId,
    incurredAt: expenseData.incurredAt || new Date(),
  });

  collective.addUserWithRole(remoteUser, roles.CONTRIBUTOR).catch(e => {
    if (e.name === 'SequelizeUniqueConstraintError') {
      console.log('User ', remoteUser.id, 'is already a contributor');
    } else {
      console.error(e);
    }
  });

  expense.user = remoteUser;
  expense.collective = collective;
  await expense.createActivity(activities.COLLECTIVE_EXPENSE_CREATED);
  return expense;
}

export async function editExpense(remoteUser, expenseData) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to edit an expense');
  }

  const expense = await models.Expense.findByPk(expenseData.id, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canEditExpense(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to edit this expense");
  }

  if (expenseData.currency && expenseData.currency !== expense.collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${expense.collective.currency})`,
    );
  }

  // When updating amount, attachment or payoutMethod, we reset its status to PENDING
  if (
    expenseData.amount !== expense.amount ||
    expenseData.payoutMethod !== expense.payoutMethod ||
    expenseData.attachment !== expense.attachment
  ) {
    expenseData.status = statuses.PENDING;
  } else {
    // make sure that we don't override the status of the expense.
    delete expenseData.status;
  }

  expenseData.lastEditedById = remoteUser.id;
  await expense.update(expenseData);
  expense.createActivity(activities.COLLECTIVE_EXPENSE_UPDATED);
  return expense;
}

export async function deleteExpense(remoteUser, expenseId) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to delete an expense');
  }

  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canEditExpense(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to delete this expense");
  }

  const res = await expense.destroy();
  return res;
}

/** Helper that finishes the process of paying an expense */
async function markExpenseAsPaid(expense) {
  debug('update expense status to PAID', expense.id);
  const updatedExpense = await expense.update({ status: statuses.PAID });
  await expense.createActivity(activities.COLLECTIVE_EXPENSE_PAID);
  return updatedExpense;
}

async function createTransactions(host, expense, fees = {}) {
  debug('marking expense as paid and creating transactions in the ledger', expense.id);
  return await createTransactionFromPaidExpense(
    host,
    null,
    expense,
    null,
    expense.UserId,
    fees.paymentProcessorFeeInHostCurrency,
    fees.hostFeeInHostCurrency,
    fees.platformFeeInHostCurrency,
  );
}

async function payExpenseWithPayPal(remoteUser, expense, host, paymentMethod, fees = {}) {
  debug('payExpenseWithPayPal', expense.id);
  try {
    const paymentResponse = await paymentProviders[expense.payoutMethod].types['adaptive'].pay(
      expense.collective,
      expense,
      expense.paypalEmail,
      paymentMethod.token,
    );
    await createTransactionFromPaidExpense(
      host,
      paymentMethod,
      expense,
      paymentResponse,
      expense.UserId,
      fees.paymentProcessorFeeInHostCurrency,
      fees.hostFeeInHostCurrency,
      fees.platformFeeInHostCurrency,
    );
    expense.setPaid(remoteUser.id);
    await paymentMethod.updateBalance();
  } catch (err) {
    debug('paypal> error', JSON.stringify(err, null, '  '));
    if (
      err.message.indexOf('The total amount of all payments exceeds the maximum total amount for all payments') !== -1
    ) {
      throw new errors.BadRequest(
        'Not enough funds in your existing Paypal preapproval. Please refill your PayPal payment balance.',
      );
    } else {
      throw new errors.BadRequest(err.message);
    }
  }
}

/**
 * Pay an expense based on the payout method defined in the Expense object
 * @PRE: fees { id, paymentProcessorFeeInCollectiveCurrency, hostFeeInCollectiveCurrency, platformFeeInCollectiveCurrency }
 * Note: some payout methods like PayPal will automatically define `paymentProcessorFeeInCollectiveCurrency`
 */
export async function payExpense(remoteUser, expenseId, fees = {}) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to pay an expense');
  }
  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });
  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }
  if (expense.status === statuses.PAID) {
    throw new errors.Unauthorized('Expense has already been paid');
  }
  if (expense.status !== statuses.APPROVED) {
    throw new errors.Unauthorized(`Expense needs to be approved. Current status of the expense: ${expense.status}.`);
  }
  if (!canUpdateExpenseStatus(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to pay this expense");
  }
  const host = await expense.collective.getHostCollective();

  // Expenses in kind can be made for collectives without any
  // funds. That's why we skip earlier here.
  if (expense.payoutMethod === 'donation') {
    const transaction = await createTransactionFromPaidExpense(host, null, expense, null, expense.UserId);
    await createTransactionFromInKindDonation(transaction);
    const user = await models.User.findByPk(expense.UserId);
    await expense.collective.addUserWithRole(user, 'BACKER');
    return markExpenseAsPaid(expense);
  }
  const balance = await expense.collective.getBalance();

  if (expense.amount > balance) {
    throw new errors.Unauthorized(
      `You don't have enough funds to pay this expense. Current balance: ${formatCurrency(
        balance,
        expense.collective.currency,
      )}, Expense amount: ${formatCurrency(expense.amount, expense.collective.currency)}`,
    );
  }

  const fxrate = await getFxRate(expense.collective.currency, host.currency);
  const feesInHostCurrency = {};
  if (paymentProviders[expense.payoutMethod]) {
    fees.paymentProcessorFeeInCollectiveCurrency = await paymentProviders[expense.payoutMethod].types['adaptive'].fees({
      amount: expense.amount,
      currency: expense.collective.currency,
      host,
    });
  }

  feesInHostCurrency.paymentProcessorFeeInHostCurrency = Math.round(
    fxrate * (fees.paymentProcessorFeeInCollectiveCurrency || 0),
  );
  feesInHostCurrency.hostFeeInHostCurrency = Math.round(fxrate * (fees.hostFeeInCollectiveCurrency || 0));
  feesInHostCurrency.platformFeeInHostCurrency = Math.round(fxrate * (fees.platformFeeInCollectiveCurrency || 0));

  if (expense.amount + fees.paymentProcessorFeeInCollectiveCurrency > balance) {
    throw new Error(
      `You don't have enough funds to cover for the fees of this payment method. Current balance: ${formatCurrency(
        balance,
        expense.collective.currency,
      )}, Expense amount: ${formatCurrency(expense.amount, expense.collective.currency)}, Estimated ${
        expense.payoutMethod
      } fees: ${formatCurrency(fees.paymentProcessorFeeInCollectiveCurrency, expense.collective.currency)}`,
    );
  }
  if (expense.payoutMethod === 'paypal') {
    expense.paypalEmail = await expense.getPaypalEmail();
    const paymentMethod = await host.getPaymentMethod({
      service: expense.payoutMethod,
    });
    // If the expense has been filed with the same paypal email than the host paypal
    // then we simply mark the expense as paid
    if (expense.paypalEmail === paymentMethod.name) {
      feesInHostCurrency.paymentProcessorFeeInHostCurrency = 0;
      await createTransactions(host, expense, feesInHostCurrency);
    } else {
      await payExpenseWithPayPal(remoteUser, expense, host, paymentMethod, feesInHostCurrency);
    }
  }

  // note: we need to check for manual and other for legacy reasons
  if (expense.payoutMethod === 'manual' || expense.payoutMethod === 'other') {
    await createTransactions(host, expense, feesInHostCurrency);
  }

  return markExpenseAsPaid(expense);
}

export async function markExpenseAsUnpaid(remoteUser, ExpenseId) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to pay an expense');
  }

  const expense = await models.Expense.findByPk(ExpenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.NotFound('No expense transaction found');
  }

  if (!canMarkExpenseUnpaid(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to mark this expense unpaid");
  }

  if (expense.status !== statuses.PAID) {
    throw new errors.Unauthorized('Expense has not been paid');
  }

  if (expense.payoutMethod !== 'other') {
    throw new errors.Unauthorized('Only expense with other payout method can be marked unpaid');
  }

  const transaction = await models.Transaction.findOne({
    where: { ExpenseId },
    include: [{ model: models.Expense }],
  });
}
<commit_msg>enhancement(expense): refund expense transaction marked as unpaid<commit_after>import { get } from 'lodash';
import errors from '../../../lib/errors';
import roles from '../../../constants/roles';
import statuses from '../../../constants/expense_status';
import activities from '../../../constants/activities';
import models from '../../../models';
import paymentProviders from '../../../paymentProviders';
import * as libPayments from '../../../lib/payments';
import { formatCurrency } from '../../../lib/utils';
import {
  createFromPaidExpense as createTransactionFromPaidExpense,
  createTransactionFromInKindDonation,
} from '../../../lib/transactions';
import { getFxRate } from '../../../lib/currency';
import debugLib from 'debug';

const debug = debugLib('expenses');

/**
 * Only admin of expense.collective or of expense.collective.host can approve/reject expenses
 */
function canUpdateExpenseStatus(remoteUser, expense) {
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.CollectiveId)) {
    return true;
  }
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.collective.HostCollectiveId)) {
    return true;
  }
  return false;
}

/**
 * Only admin of expense.collective.host can mark expenses unpaid
 */
function canMarkExpenseUnpaid(remoteUser, expense) {
  if (remoteUser.hasRole([roles.HOST, roles.ADMIN], expense.collective.HostCollectiveId)) {
    return true;
  }
  return false;
}

/**
 * Only the author or an admin of the collective or collective.host can edit an expense when it hasn't been paid yet
 */
function canEditExpense(remoteUser, expense) {
  if (expense.status === statuses.PAID) return false;
  if (remoteUser.id === expense.UserId) {
    return true;
  }
  return canUpdateExpenseStatus(remoteUser, expense);
}

export async function updateExpenseStatus(remoteUser, expenseId, status) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to update the status of an expense');
  }

  if (Object.keys(statuses).indexOf(status) === -1) {
    throw new errors.ValidationFailed('Invalid status, status must be one of ', Object.keys(statuses).join(', '));
  }

  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canUpdateExpenseStatus(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to approve this expense");
  }
  switch (status) {
    case statuses.APPROVED:
      if (expense.status === statuses.PAID) {
        throw new errors.Unauthorized("You can't reject an expense that is already paid");
      }
      break;
    case statuses.REJECTED:
      if (expense.status === statuses.PAID) {
        throw new errors.Unauthorized("You can't approve an expense that is already paid");
      }
      break;
    case statuses.PAID:
      if (expense.status !== statuses.APPROVED) {
        throw new errors.Unauthorized('The expense must be approved before you can set it to paid');
      }
      break;
  }
  const res = await expense.update({ status, lastEditedById: remoteUser.id });
  return res;
}

export async function createExpense(remoteUser, expenseData) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to create an expense');
  }
  if (!get(expenseData, 'collective.id')) {
    throw new errors.Unauthorized('Missing expense.collective.id');
  }

  // Update remoteUser's paypal email if it has changed
  if (get(expenseData, 'user.paypalEmail') !== remoteUser.paypalEmail) {
    remoteUser.paypalEmail = get(expenseData, 'user.paypalEmail');
    remoteUser.save();
  }
  expenseData.UserId = remoteUser.id;

  const collective = await models.Collective.findByPk(expenseData.collective.id);

  if (expenseData.currency && expenseData.currency !== collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${expenseData.collective.currency})`,
    );
  }

  if (!collective) {
    throw new errors.ValidationFailed('Collective not found');
  }

  if (expenseData.currency && expenseData.currency !== collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${collective.currency})`,
    );
  }

  const expense = await models.Expense.create({
    ...expenseData,
    status: statuses.PENDING,
    CollectiveId: collective.id,
    lastEditedById: expenseData.UserId,
    incurredAt: expenseData.incurredAt || new Date(),
  });

  collective.addUserWithRole(remoteUser, roles.CONTRIBUTOR).catch(e => {
    if (e.name === 'SequelizeUniqueConstraintError') {
      console.log('User ', remoteUser.id, 'is already a contributor');
    } else {
      console.error(e);
    }
  });

  expense.user = remoteUser;
  expense.collective = collective;
  await expense.createActivity(activities.COLLECTIVE_EXPENSE_CREATED);
  return expense;
}

export async function editExpense(remoteUser, expenseData) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to edit an expense');
  }

  const expense = await models.Expense.findByPk(expenseData.id, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canEditExpense(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to edit this expense");
  }

  if (expenseData.currency && expenseData.currency !== expense.collective.currency) {
    throw new errors.ValidationFailed(
      `The currency of the expense (${expenseData.currency}) needs to be the same as the currency of the collective (${expense.collective.currency})`,
    );
  }

  // When updating amount, attachment or payoutMethod, we reset its status to PENDING
  if (
    expenseData.amount !== expense.amount ||
    expenseData.payoutMethod !== expense.payoutMethod ||
    expenseData.attachment !== expense.attachment
  ) {
    expenseData.status = statuses.PENDING;
  } else {
    // make sure that we don't override the status of the expense.
    delete expenseData.status;
  }

  expenseData.lastEditedById = remoteUser.id;
  await expense.update(expenseData);
  expense.createActivity(activities.COLLECTIVE_EXPENSE_UPDATED);
  return expense;
}

export async function deleteExpense(remoteUser, expenseId) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to delete an expense');
  }

  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });

  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }

  if (!canEditExpense(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to delete this expense");
  }

  const res = await expense.destroy();
  return res;
}

/** Helper that finishes the process of paying an expense */
async function markExpenseAsPaid(expense) {
  debug('update expense status to PAID', expense.id);
  const updatedExpense = await expense.update({ status: statuses.PAID });
  await expense.createActivity(activities.COLLECTIVE_EXPENSE_PAID);
  return updatedExpense;
}

async function createTransactions(host, expense, fees = {}) {
  debug('marking expense as paid and creating transactions in the ledger', expense.id);
  return await createTransactionFromPaidExpense(
    host,
    null,
    expense,
    null,
    expense.UserId,
    fees.paymentProcessorFeeInHostCurrency,
    fees.hostFeeInHostCurrency,
    fees.platformFeeInHostCurrency,
  );
}

async function payExpenseWithPayPal(remoteUser, expense, host, paymentMethod, fees = {}) {
  debug('payExpenseWithPayPal', expense.id);
  try {
    const paymentResponse = await paymentProviders[expense.payoutMethod].types['adaptive'].pay(
      expense.collective,
      expense,
      expense.paypalEmail,
      paymentMethod.token,
    );
    await createTransactionFromPaidExpense(
      host,
      paymentMethod,
      expense,
      paymentResponse,
      expense.UserId,
      fees.paymentProcessorFeeInHostCurrency,
      fees.hostFeeInHostCurrency,
      fees.platformFeeInHostCurrency,
    );
    expense.setPaid(remoteUser.id);
    await paymentMethod.updateBalance();
  } catch (err) {
    debug('paypal> error', JSON.stringify(err, null, '  '));
    if (
      err.message.indexOf('The total amount of all payments exceeds the maximum total amount for all payments') !== -1
    ) {
      throw new errors.BadRequest(
        'Not enough funds in your existing Paypal preapproval. Please refill your PayPal payment balance.',
      );
    } else {
      throw new errors.BadRequest(err.message);
    }
  }
}

/**
 * Pay an expense based on the payout method defined in the Expense object
 * @PRE: fees { id, paymentProcessorFeeInCollectiveCurrency, hostFeeInCollectiveCurrency, platformFeeInCollectiveCurrency }
 * Note: some payout methods like PayPal will automatically define `paymentProcessorFeeInCollectiveCurrency`
 */
export async function payExpense(remoteUser, expenseId, fees = {}) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to pay an expense');
  }
  const expense = await models.Expense.findByPk(expenseId, {
    include: [{ model: models.Collective, as: 'collective' }],
  });
  if (!expense) {
    throw new errors.Unauthorized('Expense not found');
  }
  if (expense.status === statuses.PAID) {
    throw new errors.Unauthorized('Expense has already been paid');
  }
  if (expense.status !== statuses.APPROVED) {
    throw new errors.Unauthorized(`Expense needs to be approved. Current status of the expense: ${expense.status}.`);
  }
  if (!canUpdateExpenseStatus(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to pay this expense");
  }
  const host = await expense.collective.getHostCollective();

  // Expenses in kind can be made for collectives without any
  // funds. That's why we skip earlier here.
  if (expense.payoutMethod === 'donation') {
    const transaction = await createTransactionFromPaidExpense(host, null, expense, null, expense.UserId);
    await createTransactionFromInKindDonation(transaction);
    const user = await models.User.findByPk(expense.UserId);
    await expense.collective.addUserWithRole(user, 'BACKER');
    return markExpenseAsPaid(expense);
  }
  const balance = await expense.collective.getBalance();

  if (expense.amount > balance) {
    throw new errors.Unauthorized(
      `You don't have enough funds to pay this expense. Current balance: ${formatCurrency(
        balance,
        expense.collective.currency,
      )}, Expense amount: ${formatCurrency(expense.amount, expense.collective.currency)}`,
    );
  }

  const fxrate = await getFxRate(expense.collective.currency, host.currency);
  const feesInHostCurrency = {};
  if (paymentProviders[expense.payoutMethod]) {
    fees.paymentProcessorFeeInCollectiveCurrency = await paymentProviders[expense.payoutMethod].types['adaptive'].fees({
      amount: expense.amount,
      currency: expense.collective.currency,
      host,
    });
  }

  feesInHostCurrency.paymentProcessorFeeInHostCurrency = Math.round(
    fxrate * (fees.paymentProcessorFeeInCollectiveCurrency || 0),
  );
  feesInHostCurrency.hostFeeInHostCurrency = Math.round(fxrate * (fees.hostFeeInCollectiveCurrency || 0));
  feesInHostCurrency.platformFeeInHostCurrency = Math.round(fxrate * (fees.platformFeeInCollectiveCurrency || 0));

  if (expense.amount + fees.paymentProcessorFeeInCollectiveCurrency > balance) {
    throw new Error(
      `You don't have enough funds to cover for the fees of this payment method. Current balance: ${formatCurrency(
        balance,
        expense.collective.currency,
      )}, Expense amount: ${formatCurrency(expense.amount, expense.collective.currency)}, Estimated ${
        expense.payoutMethod
      } fees: ${formatCurrency(fees.paymentProcessorFeeInCollectiveCurrency, expense.collective.currency)}`,
    );
  }
  if (expense.payoutMethod === 'paypal') {
    expense.paypalEmail = await expense.getPaypalEmail();
    const paymentMethod = await host.getPaymentMethod({
      service: expense.payoutMethod,
    });
    // If the expense has been filed with the same paypal email than the host paypal
    // then we simply mark the expense as paid
    if (expense.paypalEmail === paymentMethod.name) {
      feesInHostCurrency.paymentProcessorFeeInHostCurrency = 0;
      await createTransactions(host, expense, feesInHostCurrency);
    } else {
      await payExpenseWithPayPal(remoteUser, expense, host, paymentMethod, feesInHostCurrency);
    }
  }

  // note: we need to check for manual and other for legacy reasons
  if (expense.payoutMethod === 'manual' || expense.payoutMethod === 'other') {
    await createTransactions(host, expense, feesInHostCurrency);
  }

  return markExpenseAsPaid(expense);
}

export async function markExpenseAsUnpaid(remoteUser, ExpenseId) {
  if (!remoteUser) {
    throw new errors.Unauthorized('You need to be logged in to pay an expense');
  }

  const expense = await models.Expense.findByPk(ExpenseId, {
    include: [{ model: models.Collective, as: 'collective' }, { model: models.User, as: 'User' }],
  });

  if (!expense) {
    throw new errors.NotFound('No expense transaction found');
  }

  if (!canMarkExpenseUnpaid(remoteUser, expense)) {
    throw new errors.Unauthorized("You don't have permission to mark this expense unpaid");
  }

  if (expense.status !== statuses.PAID) {
    throw new errors.Unauthorized('Expense has not been paid');
  }

  if (expense.payoutMethod !== 'other') {
    throw new errors.Unauthorized('Only expense with other payout method can be marked unpaid');
  }

  const transaction = await models.Transaction.findOne({
    where: { ExpenseId },
    include: [{ model: models.Expense }],
  });
  const refundedTransaction = await libPayments.createRefundTransaction(transaction, 0, null, expense.User);
  await libPayments.associateTransactionRefundId(transaction, refundedTransaction);

  return expense.update({ status: statuses.APPROVED, lastEditedById: remoteUser.id });
}
<|endoftext|>


<commit_before>/**
 * POST    /sms/reply     ->  handle
 */

'use strict';

var _ = require('lodash');
var Query = require('../query/query.model');
var User = require('../user/user.model');
var PhantomParser = require('./phantom.parser');
var twilio = require('twilio');

// Get list of things
exports.handle = function(req, res, next) {
  console.log(req.body);
  var fromNumber = req.body.From || '';
  var textBody = req.body.Body || '';

  User.findOne({
    number: fromNumber
  }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt
    if (err) return next(err);
    // if (!user) {
      var user = {number: fromNumber};
      // _reply('Please sign up to use our awesome service!', user);
      // return res.json(200);
    // }

    _parse(textBody, user);

    res.json(200);
  });
};

function _parse(text, user) {
  // parse the text content and find the necessary queries based on user
  // and call below text
  console.log("TEXT: " + text);

  var query = {
    query: text,
    url: "http://en.wikipedia.org/wiki/" + text.replace(' ', '_'),
    selector: '#mw-content-text p:nth-of-type(1)',
  };

  // wikipedia_check(query);

  var query2 = {
    query: "Minion_(film)",
    url: "http://en.wikipedia.org/wiki/Minions_(film)",
    selector: '#mw-content-text p:nth-of-type(1)',
    // TODO: Implement to ask something like 'Do you want to know more about next h2'
  };

  PhantomParser.parse(text, query, function (replyText) {
    console.log(text);
    _reply(replyText, user);
  });
}

function _reply(text, user) {
  var client = twilio();
  client.sendMessage({
      to: user.number,
      from: process.env.TWILIO_NUMBER,
      body: text
  }, function(error, message) {
      if (!error) {
          console.log('Success! The SID for this SMS message is:');
          console.log(message.sid);
          console.log('Message sent on:');
          console.log(message.dateCreated);
      } else {
          console.log('Oops! There was an error.');
          console.log(error);
      }
  });
}

function handleError(res, err) {
  return res.send(500, err);
}
<commit_msg>Added query parsing.<commit_after>/**
 * POST    /sms/reply     ->  handle
 */

'use strict';

var _ = require('lodash');
var Query = require('../query/query.model');
var User = require('../user/user.model');
var PhantomParser = require('./phantom.parser');
var twilio = require('twilio');
var slugify = require('underscore.string/slugify');
var levenshtein = require('underscore.string/levenshtein');

// Get list of things
exports.handle = function(req, res, next) {
  console.log(req.body);
  var fromNumber = req.body.From || '';
  var textBody = req.body.Body || '';

  User.findOne({
    number: fromNumber
  }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt
    if (err) return next(err);
    // if (!user) {
      var user = {number: fromNumber};
      // _reply('Please sign up to use our awesome service!', user);
      // return res.json(200);
    // }

    _parse(textBody, user);

    res.json(200);
  });
};

function _parse(text, user) {
  // parse the text content and find the necessary queries based on user
  // and call below text
  var i = s.indexOf(':');
  var queryType = s.slice(0,i);
  var queryString = s.slice(i+1);
  queryType = queryType.replace(/\s+/g, '');
  queryString = queryString.trim();
  queryString = slugify(queryString);

  Query.findOne({
    query : queryType
  }, function(err, result) {
    if(err) {
      _reply("Internal error. Could unfortunatley not handle query. Please try again later.", user);
      return;
    }
    if(!result) {
      var minDistance = Number.MAX_VALUE;
      var curDistance = Number.MAX_VALUE;
      var minDistanceQueryTypeStr = "";
      Query.find({
        $or: [
          {user: user._id},
          {user: 'default'}
        ]
      }, function (err, result){
        if(err) {
          _reply("Internal error. Could unfortunatley not handle query. Please try again later.", user);
          return;
        }
        if(!result) {
          _reply("Internal error. There are unfortunatley no queries defined yet. Please try again later.", user);
          return;
        }
        // iterate through queries and find smallest levensthein distance and ask user if they meant that
        for (var i = result.length - 1; i >= 0; i--) {
          curDistance = levenshtein(result[i].query, minDistanceQueryTypeStr);
          if(curDistance < minDistance) {
            minDistance = curDistance;
            minDistanceQueryTypeStr = result[i].query;
          }
        };
        _reply("Could not find query type. Did you maybe mean to write:%0a%0a" + minDistanceQueryTypeStr + " : " + queryString);
        return;
      });
    } else {
      PhantomParser.parse(queryStr, result, function (replyText) {
        console.log(text); // @todo remove later
        _reply(replyText, user);
      });
    }
  });
}

function _reply(text, user) {
  var client = twilio();
  client.sendMessage({
      to: user.number,
      from: process.env.TWILIO_NUMBER,
      body: text
  }, function(error, message) {
      if (!error) {
          console.log('Success! The SID for this SMS message is:');
          console.log(message.sid);
          console.log('Message sent on:');
          console.log(message.dateCreated);
      } else {
          console.log('Oops! There was an error.');
          console.log(error);
      }
  });
}

function handleError(res, err) {
  return res.send(500, err);
}
<|endoftext|>