From 8843d8cccb8f20a14aa372a18fcc0a7a800084de Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Mon, 16 Jul 2018 16:42:11 +0530 Subject: [PATCH 01/19] Delete circle.yml --- circle.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 circle.yml diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 1ef76a5..0000000 --- a/circle.yml +++ /dev/null @@ -1,24 +0,0 @@ -general: - build_dir: consumer - -machine: - services: - - docker - -dependencies: - pre: - - pip install awsebcli - -test: - override: - - npm run test - -deployment: - development: - branch: dev - commands: - - ./deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM - production: - branch: master - commands: - - ./deploy/eb-deploy.sh tc-connect2sf PROD $CIRCLE_BUILD_NUM From 7fc3556107c15b7fbea4a0c9d0fa42a0ba4ec7ef Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Mon, 16 Jul 2018 16:43:19 +0530 Subject: [PATCH 02/19] Create circle.yml --- .circleci/circle.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .circleci/circle.yml diff --git a/.circleci/circle.yml b/.circleci/circle.yml new file mode 100644 index 0000000..058bd1d --- /dev/null +++ b/.circleci/circle.yml @@ -0,0 +1,24 @@ +general: + build_dir: consumer + +machine: + services: + - docker + +dependencies: + pre: + - pip install awsebcli + +test: + override: + - npm run test + +deployment: + development: + branch: [ dev, dev-circleci2 ] + commands: + - ./deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM + production: + branch: master + commands: + - ./deploy/eb-deploy.sh tc-connect2sf PROD $CIRCLE_BUILD_NUM From 1471862cd97a02c16cc810cb265dc6607929c869 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Mon, 16 Jul 2018 16:48:29 +0530 Subject: [PATCH 03/19] Update circle.yml --- .circleci/circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/circle.yml b/.circleci/circle.yml index 058bd1d..d394395 100644 --- a/.circleci/circle.yml +++ b/.circleci/circle.yml @@ -17,8 +17,8 @@ deployment: development: branch: [ dev, dev-circleci2 ] commands: - - ./deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM + - ./consumer/deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM production: branch: master commands: - - ./deploy/eb-deploy.sh tc-connect2sf PROD $CIRCLE_BUILD_NUM + - ./consumer/deploy/eb-deploy.sh tc-connect2sf PROD $CIRCLE_BUILD_NUM From f0b6d7cde2a86ee73397f9a2352c958bdf06fa50 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Mon, 16 Jul 2018 16:50:51 +0530 Subject: [PATCH 04/19] Delete circle.yml --- .circleci/circle.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .circleci/circle.yml diff --git a/.circleci/circle.yml b/.circleci/circle.yml deleted file mode 100644 index d394395..0000000 --- a/.circleci/circle.yml +++ /dev/null @@ -1,24 +0,0 @@ -general: - build_dir: consumer - -machine: - services: - - docker - -dependencies: - pre: - - pip install awsebcli - -test: - override: - - npm run test - -deployment: - development: - branch: [ dev, dev-circleci2 ] - commands: - - ./consumer/deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM - production: - branch: master - commands: - - ./consumer/deploy/eb-deploy.sh tc-connect2sf PROD $CIRCLE_BUILD_NUM From 139e4d88b79e5483051541647b41be7982d67250 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Mon, 16 Jul 2018 16:51:50 +0530 Subject: [PATCH 05/19] Create config.yml --- .circleci/config.yml | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..573a9b9 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,76 @@ +version: 2 + +# Node image for node project +node_env: &node_env + docker: + - image: circleci/node:6.14.3-stretch-browsers + +# Python image to run aws utilities +python_env: &python_env + docker: + - image: circleci/python:2.7-stretch-browsers + +# Instructions of installing aws utilities +install_awscli: &install_awscli + name: "Install awscli" + command: | + sudo pip install awscli awsebcli --upgrade + aws --version + eb --version + +# Instructions of deployment +deploy_steps: &deploy_steps + - checkout + - attach_workspace: + at: . + - run: *install_awscli + - setup_remote_docker + - run: cd consumer && ./deploy/eb-deploy.sh tc-connect2sf DEV $CIRCLE_BUILD_NUM + +jobs: + build: + <<: *node_env + steps: + - checkout + - restore_cache: + key: node-modules-{{ checksum "consumer/package.json" }} + - run: cd consumer && npm install + - save_cache: + key: node-modules-{{ checksum "consumer/package.json" }} + paths: + - consumer/node_modules + - run: cd consumer && npm run test + - persist_to_workspace: + root: . + paths: + - ./consumer/node_modules + + deploy_prod: + <<: *python_env + environment: + DEPLOY_ENV: "PROD" + steps: *deploy_steps + + deploy_dev: + <<: *python_env + environment: + DEPLOY_ENV: "DEV" + steps: *deploy_steps + +workflows: + version: 2 + build-and-deploy: + jobs: + - build + - deploy_dev: + filters: + branches: + only: [ dev, dev-circleci2 ] + requires: + - build + - deploy_prod: + filters: + branches: + only: master + requires: + - build From 3ec1dceb648a657f26d67a9358f22db82b1a97de Mon Sep 17 00:00:00 2001 From: Sharathkumar Anbu Date: Mon, 24 Sep 2018 15:57:22 +0530 Subject: [PATCH 06/19] Create Rest API --- consumer/README.md | 23 ++++ consumer/config/constants.js | 6 ++ .../config/custom-environment-variables.json | 6 +- consumer/config/sample-local.json | 7 +- .../tc-connects2f.postman_collection.json | 102 ++++++++++++++++++ .../tc-connects2f.postman_environment.json | 23 ++++ consumer/package.json | 4 + consumer/src/services/LeadService.js | 35 ++++++ consumer/src/worker.js | 82 +++++++++++--- 9 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 consumer/docs/tc-connects2f.postman_collection.json create mode 100644 consumer/docs/tc-connects2f.postman_environment.json create mode 100644 consumer/src/services/LeadService.js diff --git a/consumer/README.md b/consumer/README.md index 214edd3..d17f95d 100644 --- a/consumer/README.md +++ b/consumer/README.md @@ -255,3 +255,26 @@ For example: duplicated project id added to the queue, Lead cannot be found etc. In such situation, the message from rabbitmq will be marked as ACK (removed). If we won't remove it from queue, the message will be stuck forever. For any other type of error the message from the rabbitmq will me marked as ACK as well, however, it would requeued into another queue for later inspection. It right now publishes the message content to the same rabbitmq exchange (configured as mentioned in Configuration section) with routing key being `connect2sf.failed`. So, we have to map the exchange and routing key comibation to a queue to which no consumer is listeting e.g. `tc-connect2sf.failed` is used in dev environment. Now we can see messages, via rabbitmq manager UI, in this queue to check if any of the messages failed and what was id of the project which failed. We can either remove those messages from the queue, if we are going to add those leads manually in saleforce or move them again to the original queue after fixing the deployed environment. + + +## Express Rest API verification + +## Pre-requisites + +1. Postman + +### Steps to verify + +1. Open Postman + +2. Import Postman collection and environment from `docs` directory + +3. Assuming that API is served at http://localhost:3150/v5 and AUTH_SECRET used in the API is `mysecret`, requests in the Postman collection can be triggered + +4. Requests in Postman collection covers the status codes 200, 400 and 403 + +### Notes + +1. Express Rest API will be served at http://localhost:3150/v5 if you use the values from `config/sample-local.json`. + +2. JWT token in the Postman collection is signed with secret `mysecret` \ No newline at end of file diff --git a/consumer/config/constants.js b/consumer/config/constants.js index 1d4c5af..462099d 100644 --- a/consumer/config/constants.js +++ b/consumer/config/constants.js @@ -7,3 +7,9 @@ export const EVENT = { FAILED_SUFFIX: '.failed' }, }; + +export const ERROR = { + SERVER_ERROR: 500, + CLIENT_ERROR: 400, + MESSAGE: 'Internal Server Error' +}; \ No newline at end of file diff --git a/consumer/config/custom-environment-variables.json b/consumer/config/custom-environment-variables.json index 510378d..f4cae3e 100644 --- a/consumer/config/custom-environment-variables.json +++ b/consumer/config/custom-environment-variables.json @@ -27,5 +27,9 @@ "project": "QUEUE_PROJECTS", "connect2sf": "QUEUE_CONNECT2SF" } - } + }, + "apiVersion": "API_VERSION", + "port": "PORT", + "authSecret": "AUTH_SECRET", + "validIssuers": "VALID_ISSUERS" } diff --git a/consumer/config/sample-local.json b/consumer/config/sample-local.json index 1c72b90..2c7936b 100644 --- a/consumer/config/sample-local.json +++ b/consumer/config/sample-local.json @@ -2,6 +2,7 @@ "logLevel": "error", "rabbitmqURL": "****UPDATE****", "ownerId": "****UPDATE****", + "scheduledWorkerSchedule": "* * * 1 * *", "aws": { "endpoint": "http://dockerhost:7777", "region": "us-east-1", @@ -23,5 +24,9 @@ "queues": { "project": "dev.project.service" } - } + }, + "apiVersion": "/v5", + "port": 3150, + "authSecret": "mysecret", + "validIssuers": "[\"https://api.topcoder.com\",\"https://topcoder-dev.auth0.com\", \"https://topcoder-newauth.auth0.com\"]" } \ No newline at end of file diff --git a/consumer/docs/tc-connects2f.postman_collection.json b/consumer/docs/tc-connects2f.postman_collection.json new file mode 100644 index 0000000..97aa00a --- /dev/null +++ b/consumer/docs/tc-connects2f.postman_collection.json @@ -0,0 +1,102 @@ +{ + "info": { + "_postman_id": "23a8d300-23f5-40e8-ae69-edd7b7ff357e", + "name": "TC Connect S2F", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Valid LeadInfo request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\": \"Test\",\n\t\"lastName\": \"Work\",\n\t\"businessEmail\": \"abc@tes\",\n\t\"title\": \"Mr\",\n\t\"companyName\": \"Topcoder\",\n\t\"companySize\": \"Big\",\n\t\"userName\": \"abcd\"\n}" + }, + "url": { + "raw": "{{URL}}/connect2sf/leadInfo", + "host": [ + "{{URL}}" + ], + "path": [ + "connect2sf", + "leadInfo" + ] + } + }, + "response": [] + }, + { + "name": "Invalid LeadInfo request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\": \"Test\",\n\t\"lastName\": \"Work\",\n\t\"businessEmail\": \"abc@tes\",\n\t\"title\": \"Mr\",\n\t\"companyName\": \"Topcoder\",\n\t\"companySize\": \"Big\"\n}" + }, + "url": { + "raw": "{{URL}}/connect2sf/leadInfo", + "host": [ + "{{URL}}" + ], + "path": [ + "connect2sf", + "leadInfo" + ] + } + }, + "response": [] + }, + { + "name": "LeadInfo request with invalid token", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer 123" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\": \"Test\",\n\t\"lastName\": \"Work\",\n\t\"businessEmail\": \"abc@tes\",\n\t\"title\": \"Mr\",\n\t\"companyName\": \"Topcoder\",\n\t\"companySize\": \"Big\",\n\t\"userName\": \"abcd\"\n}" + }, + "url": { + "raw": "{{URL}}/connect2sf/leadInfo", + "host": [ + "{{URL}}" + ], + "path": [ + "connect2sf", + "leadInfo" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/consumer/docs/tc-connects2f.postman_environment.json b/consumer/docs/tc-connects2f.postman_environment.json new file mode 100644 index 0000000..3b4af56 --- /dev/null +++ b/consumer/docs/tc-connects2f.postman_environment.json @@ -0,0 +1,23 @@ +{ + "id": "493febdc-2611-442a-ae08-b42ed24688fd", + "name": "tc-connects2f", + "values": [ + { + "key": "ADMIN_TOKEN", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJBZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6IlRvbnlKIiwiZXhwIjo1NTUzMDE5OTI1OSwidXNlcklkIjoiNDA0MzMyODgiLCJpYXQiOjE1MzAxOTg2NTksImVtYWlsIjoiYWRtaW5AdG9wY29kZXIuY29tIiwianRpIjoiYzNhYzYwOGEtNTZiZS00NWQwLThmNmEtMzFmZTk0Yjk1NjFjIn0.pIHUtMwIV07ZgfaUk9916X49rgjKclM9kzQP419LBo0", + "description": "", + "type": "text", + "enabled": true + }, + { + "key": "URL", + "value": "http://localhost:3150/v5", + "description": "", + "type": "text", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-09-24T08:55:49.448Z", + "_postman_exported_using": "Postman/6.1.3" +} \ No newline at end of file diff --git a/consumer/package.json b/consumer/package.json index 64c3dc3..1a2acb3 100644 --- a/consumer/package.json +++ b/consumer/package.json @@ -41,14 +41,18 @@ "babel-preset-stage-0": "^6.5.0", "babel-runtime": "^6.23.0", "better-npm-run": "0.0.10", + "body-parser": "^1.18.3", "config": "^1.21.0", + "cors": "^2.8.4", "debug": "^2.2.0", + "express": "^4.16.3", "joi": "^9.0.4", "jsonwebtoken": "^7.1.7", "lodash": "^4.14.2", "node-cron": "^1.1.3", "superagent": "^2.1.0", "superagent-promise": "^1.1.0", + "tc-core-library-js": "appirio-tech/tc-core-library-js.git", "winston": "^2.2.0" }, "devDependencies": { diff --git a/consumer/src/services/LeadService.js b/consumer/src/services/LeadService.js new file mode 100644 index 0000000..51dbdac --- /dev/null +++ b/consumer/src/services/LeadService.js @@ -0,0 +1,35 @@ +/** + * Represents the Rest API service for leads + */ + +import Joi from 'joi'; +import {logAndValidate} from '../common/decorators'; + +const postLeadSchema = Joi.object().keys({ + reqBody: Joi.object().keys({ + firstName: Joi.string().required(), + lastName: Joi.string().required(), + businessEmail: Joi.string().email().required(), + title: Joi.string().required(), + companyName: Joi.string().required(), + companySize: Joi.string().required(), + userName: Joi.string().required(), + }), +}).required(); + +class LeadService { + + /** + * Post the lead info to Salesforce + * @param {Object} reqBody Request body + * @returns {Object} sample response + */ + @logAndValidate(['reqBody'], postLeadSchema) + postLead(reqBody) { // eslint-disable-line no-unused-vars + // TODO -- Replace with actual functions later + return {success: true}; + } + +} + +export default new LeadService(); diff --git a/consumer/src/worker.js b/consumer/src/worker.js index 7fc835a..3f869f7 100644 --- a/consumer/src/worker.js +++ b/consumer/src/worker.js @@ -4,12 +4,17 @@ import config from 'config'; import amqp from 'amqplib'; +import express from 'express'; +import cors from 'cors'; +import cron from 'node-cron'; +import bodyParser from 'body-parser'; import _ from 'lodash'; +import { middleware } from 'tc-core-library-js'; import logger from './common/logger'; import ConsumerService from './services/ConsumerService'; -import { EVENT } from '../config/constants'; -import cron from 'node-cron'; -import { start as scheduleStart } from './scheduled-worker' +import LeadService from './services/LeadService'; +import { EVENT, ERROR } from '../config/constants'; +import { start as scheduleStart } from './scheduled-worker'; const debug = require('debug')('app:worker'); @@ -24,8 +29,8 @@ process.once('SIGINT', () => { let EVENT_HANDLERS = { [EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED]: ConsumerService.processProjectCreated, - [EVENT.ROUTING_KEY.PROJECT_UPDATED]: ConsumerService.processProjectUpdated -} + [EVENT.ROUTING_KEY.PROJECT_UPDATED]: ConsumerService.processProjectUpdated, +}; export function initHandlers(handlers) { EVENT_HANDLERS = handlers; @@ -59,7 +64,7 @@ export async function consume(channel, exchangeName, queue, publishChannel) { handler = EVENT_HANDLERS[key]; if (!_.isFunction(handler)) { logger.error(`Unknown message type: ${key}, NACKing... `); - channel.nack(msg, false, false) + channel.nack(msg, false, false); } data = JSON.parse(msg.content.toString()); } catch (ignore) { @@ -88,7 +93,7 @@ export async function consume(channel, exchangeName, queue, publishChannel) { key, new Buffer(msg.content.toString()) ); - } catch(e) { + } catch (e) { // TODO decide if we want nack the original msg here // for now just ignoring the error in requeue logger.logFullError(e, `Error in publising Exchange to ${exchangeName}`); @@ -102,11 +107,11 @@ export async function consume(channel, exchangeName, queue, publishChannel) { /** * Start the worker */ -async function start() { +async function startWorker() { try { - console.log("Worker Connecting to RabbitMQ: " + config.rabbitmqURL.substr(-5)); + console.log(`Worker Connecting to RabbitMQ: ${config.rabbitmqURL.substr(-5)}`); connection = await amqp.connect(config.rabbitmqURL); - debug('created connection successfully with URL: ' + config.rabbitmqURL); + debug(`created connection successfully with URL: ${config.rabbitmqURL}`); const channel = await connection.createConfirmChannel(); debug('Channel created for projects exchange ...'); const publishChannel = await connection.createConfirmChannel(); @@ -122,10 +127,61 @@ async function start() { } } +/* + * Error handler for Async functions + */ +const asyncHandler = fn => (req, res, next) => { + Promise + .resolve(fn(req, res, next)) + .catch(next); +}; + if (!module.parent) { - start(); - - cron.schedule(config.scheduledWorkerSchedule, function(){ + startWorker(); + + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development'; + } + + const app = express(); + + app.use(cors()); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ + extended: true, + })); + + app.use((req, res, next) => { + middleware.jwtAuthenticator({ + AUTH_SECRET: config.authSecret, + VALID_ISSUERS: config.validIssuers, + })(req, res, next); + }); + + app.post(`${config.apiVersion}/connect2sf/leadInfo`, asyncHandler(async (req, res, next) => { + const result = await LeadService.postLead(req.body); + res.json(result); + })); + + // Error handler + app.use(async (err, req, res, next) => { + let status = ERROR.SERVER_ERROR; + let message = err.message; + // Fetch actual error message from details for JOI errors + if (err.isJoi) { + status = ERROR.CLIENT_ERROR; + message = err.details[0].message; + } + if (!message) { + message = ERROR.MESSAGE; + } + res.status(status).send({ message }); + }); + + app.listen(config.port); + debug(`Express server listening on port ${config.port} in ${process.env.NODE_ENV} mode`); + + cron.schedule(config.scheduledWorkerSchedule, () => { scheduleStart(); }); } From 02cac9f7a957d6ca98d07479137ee42007bf7157 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 26 Sep 2018 17:20:09 +0530 Subject: [PATCH 07/19] Added salesforce lead creation logic to the lead endpoint Updated existing lead creation logic on project creation, to update existing one if exists --- consumer/src/services/ConsumerService.js | 38 +++++++++++++-------- consumer/src/services/LeadService.js | 42 ++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/consumer/src/services/ConsumerService.js b/consumer/src/services/ConsumerService.js index 77bba75..97a1447 100644 --- a/consumer/src/services/ConsumerService.js +++ b/consumer/src/services/ConsumerService.js @@ -83,7 +83,7 @@ class ConsumerService { const campaignId = responses[0]; const user = responses[1]; const { accessToken, instanceUrl } = responses[2]; - const lead = { + const leadData = { FirstName: user.firstName, LastName: user.lastName, Email: user.email, @@ -98,18 +98,31 @@ class ConsumerService { TC_Connect_Cancel_Reason__c: _.get(project,"cancelReason",""), TC_Connect_Raw_Project__c: JSON.stringify(project), }; - return SalesforceService.createObject('Lead', lead, accessToken, instanceUrl) - .then((leadId) => { - const campaignMember = { - LeadId: leadId, - CampaignId: campaignId, - }; - return SalesforceService.createObject('CampaignMember', campaignMember, accessToken, instanceUrl); - }).catch( (e) => { - if (e.response && e.response.text && duplicateRecordRegex.test(e.response.text)) { - throw new UnprocessableError(`Lead already existing for project ${project.id}`); + let sql = `SELECT id,IsConverted FROM Lead WHERE Email = '${project.id}' AND LeadSource = 'Connect'`; + return SalesforceService.query(sql, accessToken, instanceUrl) + .then((response) => { + const {records: [lead]} = response; + if (!lead) { + // if lead does not exists, create new one + return SalesforceService.createObject('Lead', leadData, accessToken, instanceUrl) + .then((leadId) => { + const campaignMember = { + LeadId: leadId, + CampaignId: campaignId, + }; + return SalesforceService.createObject('CampaignMember', campaignMember, accessToken, instanceUrl); + }).catch( (e) => { + if (e.response && e.response.text && duplicateRecordRegex.test(e.response.text)) { + throw new UnprocessableError(`Lead already existing for project ${project.id}`); + } + throw e; + }) + } else { + // if lead does exists update it with project data + if (lead.IsConverted != true && !_.isEmpty(leadData)) { + return SalesforceService.updateObject(lead.Id, 'Lead', leadData, accessToken, instanceUrl); + } } - throw e; }) }).catch((error) => { throw error; @@ -127,7 +140,6 @@ class ConsumerService { var project = projectEvent.original; var projectUpdated = projectEvent.updated; - return Promise.all([ ConfigurationService.getSalesforceCampaignId(), SalesforceService.authenticate(), diff --git a/consumer/src/services/LeadService.js b/consumer/src/services/LeadService.js index 51dbdac..8997f7b 100644 --- a/consumer/src/services/LeadService.js +++ b/consumer/src/services/LeadService.js @@ -3,7 +3,10 @@ */ import Joi from 'joi'; +import config from 'config'; import {logAndValidate} from '../common/decorators'; +import ConfigurationService from './ConfigurationService'; +import SalesforceService from './SalesforceService'; const postLeadSchema = Joi.object().keys({ reqBody: Joi.object().keys({ @@ -17,6 +20,8 @@ const postLeadSchema = Joi.object().keys({ }), }).required(); +const leadSource = 'Connect'; + class LeadService { /** @@ -25,9 +30,40 @@ class LeadService { * @returns {Object} sample response */ @logAndValidate(['reqBody'], postLeadSchema) - postLead(reqBody) { // eslint-disable-line no-unused-vars - // TODO -- Replace with actual functions later - return {success: true}; + postLead(user) { // eslint-disable-line no-unused-vars + let leadId = 0; + return Promise.all([ + ConfigurationService.getSalesforceCampaignId(), + SalesforceService.authenticate(), + ]).then((responses) => { + const campaignId = responses[0]; + const { accessToken, instanceUrl } = responses[1]; + const lead = { + FirstName: user.firstName, + LastName: user.lastName, + Email: user.businessEmail, + LeadSource: leadSource, + Company: user.companyName, + No_of_Employees__c: user.companySize, + OwnerId: config.ownerId, + TC_Handle__c: user.userName, + }; + return SalesforceService.createObject('Lead', lead, accessToken, instanceUrl) + .then((_leadId) => { + leadId = _leadId; + const campaignMember = { + LeadId: _leadId, + CampaignId: campaignId, + }; + return SalesforceService.createObject('CampaignMember', campaignMember, accessToken, instanceUrl); + }).catch( (e) => { + throw e; + }) + }).then(() => { + return {success: true, leadId }; + }).catch((error) => { + throw error; + }); } } From d86b19881cc30df5c4887afbc369d7ae519cf984 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 26 Sep 2018 17:42:06 +0530 Subject: [PATCH 08/19] debug --- consumer/src/services/SalesforceService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/consumer/src/services/SalesforceService.js b/consumer/src/services/SalesforceService.js index 17e6f5c..6f2c9cb 100644 --- a/consumer/src/services/SalesforceService.js +++ b/consumer/src/services/SalesforceService.js @@ -17,6 +17,8 @@ const request = superagentPromise(superagent, Promise); let privateKey = process.env.SALESFORCE_CLIENT_KEY || 'privateKey' privateKey = privateKey.replace(/\\n/g, "\n") +console.log(privateKey); + const createObjectSchema = { type: Joi.string().required(), params: Joi.object().required(), From ef1892b43732a868bb24c5d5322678dd897c7353 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 26 Sep 2018 18:00:24 +0530 Subject: [PATCH 09/19] removed debug --- consumer/src/services/SalesforceService.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/consumer/src/services/SalesforceService.js b/consumer/src/services/SalesforceService.js index 6f2c9cb..17e6f5c 100644 --- a/consumer/src/services/SalesforceService.js +++ b/consumer/src/services/SalesforceService.js @@ -17,8 +17,6 @@ const request = superagentPromise(superagent, Promise); let privateKey = process.env.SALESFORCE_CLIENT_KEY || 'privateKey' privateKey = privateKey.replace(/\\n/g, "\n") -console.log(privateKey); - const createObjectSchema = { type: Joi.string().required(), params: Joi.object().required(), From 5a915984371032c69a48e3a61ebe78cc604b5fcc Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 26 Sep 2018 18:05:46 +0530 Subject: [PATCH 10/19] temp disabling the unit tests --- consumer/test/ConsumerService.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/consumer/test/ConsumerService.spec.js b/consumer/test/ConsumerService.spec.js index 08ffac1..82a7af4 100644 --- a/consumer/test/ConsumerService.spec.js +++ b/consumer/test/ConsumerService.spec.js @@ -82,7 +82,7 @@ describe('ConsumerService', () => { }); describe('processProjectCreated', () => { - it('should process project successfully', async() => { + xit('should process project successfully', async() => { const expectedLead = { FirstName: 'john', @@ -131,7 +131,7 @@ describe('ConsumerService', () => { } }); - it('should throw UnprocessableError if Lead already exists', async() => { + xit('should throw UnprocessableError if Lead already exists', async() => { const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => { const err = new Error('Bad request'); err.response = { @@ -145,7 +145,7 @@ describe('ConsumerService', () => { createObjectStub.should.have.been.called; }); - it('should rethrow Error from createObject if error is not duplicate', async() => { + xit('should rethrow Error from createObject if error is not duplicate', async() => { const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => { throw new Error('Fake Error'); }); From 1920b6fb03250717b355704ddbb72e3aac56a529 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 10:53:18 +0530 Subject: [PATCH 11/19] Ignoring 400 error code because it is causing the process to be killed. --- consumer/src/services/ConsumerService.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/consumer/src/services/ConsumerService.js b/consumer/src/services/ConsumerService.js index 97a1447..3baa71f 100644 --- a/consumer/src/services/ConsumerService.js +++ b/consumer/src/services/ConsumerService.js @@ -125,6 +125,7 @@ class ConsumerService { } }) }).catch((error) => { + error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data throw error; }); } @@ -171,7 +172,12 @@ class ConsumerService { // } // return SalesforceService.deleteObject('CampaignMember', member.Id, accessToken, instanceUrl); // }) - }) + }).catch((error) => { + if (error.status === 400) { + error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data + return Promise.reject(error); + } + }); }); } } From b4f81a51c336cd7b6f3cd0041fdf4ee953fbf499 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 10:55:07 +0530 Subject: [PATCH 12/19] fixing unit test --- consumer/src/services/ConsumerService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consumer/src/services/ConsumerService.js b/consumer/src/services/ConsumerService.js index 3baa71f..e6896ad 100644 --- a/consumer/src/services/ConsumerService.js +++ b/consumer/src/services/ConsumerService.js @@ -175,7 +175,7 @@ class ConsumerService { }).catch((error) => { if (error.status === 400) { error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data - return Promise.reject(error); + throw error; } }); }); From ce593edee4046e6b7b40aee1f991f7c0feb308bc Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 10:57:35 +0530 Subject: [PATCH 13/19] fixed tests --- consumer/src/services/ConsumerService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/consumer/src/services/ConsumerService.js b/consumer/src/services/ConsumerService.js index e6896ad..1f71295 100644 --- a/consumer/src/services/ConsumerService.js +++ b/consumer/src/services/ConsumerService.js @@ -125,7 +125,9 @@ class ConsumerService { } }) }).catch((error) => { - error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data + if (error.status === 400) { + error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data + } throw error; }); } @@ -175,8 +177,8 @@ class ConsumerService { }).catch((error) => { if (error.status === 400) { error.shouldAck = true; // ignore bad requests, most probably it is because of malformed data - throw error; } + throw error; }); }); } From 0977586542e287c608137b9a6aa69bb010cebe6f Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 14:43:40 +0530 Subject: [PATCH 14/19] Fixed unit tests --- consumer/src/services/ConsumerService.js | 2 +- consumer/test/ConsumerService.spec.js | 52 ++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/consumer/src/services/ConsumerService.js b/consumer/src/services/ConsumerService.js index 1f71295..40d045f 100644 --- a/consumer/src/services/ConsumerService.js +++ b/consumer/src/services/ConsumerService.js @@ -98,7 +98,7 @@ class ConsumerService { TC_Connect_Cancel_Reason__c: _.get(project,"cancelReason",""), TC_Connect_Raw_Project__c: JSON.stringify(project), }; - let sql = `SELECT id,IsConverted FROM Lead WHERE Email = '${project.id}' AND LeadSource = 'Connect'`; + let sql = `SELECT id,IsConverted FROM Lead WHERE Email = '${user.email}' AND LeadSource = 'Connect'`; return SalesforceService.query(sql, accessToken, instanceUrl) .then((response) => { const {records: [lead]} = response; diff --git a/consumer/test/ConsumerService.spec.js b/consumer/test/ConsumerService.spec.js index 82a7af4..1da74c4 100644 --- a/consumer/test/ConsumerService.spec.js +++ b/consumer/test/ConsumerService.spec.js @@ -82,8 +82,8 @@ describe('ConsumerService', () => { }); describe('processProjectCreated', () => { - xit('should process project successfully', async() => { - + it('should process project successfully when lead does not exists', async() => { + const leadSql = `SELECT id,IsConverted FROM Lead WHERE Email = 'jd@example.com' AND LeadSource = 'Connect'`; const expectedLead = { FirstName: 'john', LastName: 'doe', @@ -105,17 +105,59 @@ describe('ConsumerService', () => { CampaignId: sfCampaignId, }; + const queryStub = sandbox.stub(SalesforceService, 'query'); + queryStub.onCall(0) + .returns(Promise.resolve({ records: [] })); const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => leadId); await ConsumerService.processProjectCreated(logger, project); getCampaignIdStub.should.have.been.called; getUserStub.should.have.been.calledWith(userId); authenticateStub.should.have.been.called; + queryStub.should.have.been.calledWith(leadSql, sfAuth.accessToken, sfAuth.instanceUrl); createObjectStub.should.have.been.calledWith('Lead', expectedLead, sfAuth.accessToken, sfAuth.instanceUrl); createObjectStub.should.have.been.calledWith('CampaignMember', expectedCampaignMember, sfAuth.accessToken, sfAuth.instanceUrl); }); + it('should process project successfully when lead does exists', async() => { + const leadSql = `SELECT id,IsConverted FROM Lead WHERE Email = 'jd@example.com' AND LeadSource = 'Connect'`; + const expectedLead = { + FirstName: 'john', + LastName: 'doe', + Email: 'jd@example.com', + LeadSource: 'Connect', + Company: 'Unknown', + OwnerId: config.ownerId, + TC_Handle__c: 'jdoe', + TC_Connect_Project_Id__c: 1, + TC_Connect_Project_Status__c: '', + TC_Connect_Cancel_Reason__c: null, + TC_Connect_Direct_Project_Id__c: '', + TC_Connect_Description__c:'', + TC_Connect_Raw_Project__c: JSON.stringify(project) + }; + + const expectedCampaignMember = { + LeadId: leadId, + CampaignId: sfCampaignId, + }; + + const queryStub = sandbox.stub(SalesforceService, 'query'); + queryStub.onCall(0) + .returns(Promise.resolve({ records: [{ Id: leadId }] })); + const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => leadId); + const updateStub = sandbox.stub(SalesforceService,'updateObject', async() => {}); + + await ConsumerService.processProjectCreated(logger, project); + getCampaignIdStub.should.have.been.called; + getUserStub.should.have.been.calledWith(userId); + authenticateStub.should.have.been.called; + createObjectStub.should.not.have.been.called; + queryStub.should.have.been.calledWith(leadSql, sfAuth.accessToken, sfAuth.instanceUrl); + updateStub.should.have.been.calledWith(leadId, 'Lead', expectedLead, sfAuth.accessToken, sfAuth.instanceUrl); + }); + it('should throw UnprocessableError primary customer is not found', async() => { const projectWihoutMembers = { id: 1, @@ -131,6 +173,7 @@ describe('ConsumerService', () => { } }); + // Not a valid use case any more, we now allow updation of existing lead in project creation event as well xit('should throw UnprocessableError if Lead already exists', async() => { const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => { const err = new Error('Bad request'); @@ -145,7 +188,10 @@ describe('ConsumerService', () => { createObjectStub.should.have.been.called; }); - xit('should rethrow Error from createObject if error is not duplicate', async() => { + it('should rethrow Error from createObject if error is not duplicate', async() => { + const queryStub = sandbox.stub(SalesforceService, 'query'); + queryStub.onCall(0) + .returns(Promise.resolve({ records: [] })); const createObjectStub = sandbox.stub(SalesforceService, 'createObject', async() => { throw new Error('Fake Error'); }); From 1b4253185729a2468ed778058190325fa4f69b1a Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 14:58:46 +0530 Subject: [PATCH 15/19] Added PORT env variable --- consumer/.ebextensions/01-environment-variables.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/consumer/.ebextensions/01-environment-variables.config b/consumer/.ebextensions/01-environment-variables.config index 7e2710c..6979473 100644 --- a/consumer/.ebextensions/01-environment-variables.config +++ b/consumer/.ebextensions/01-environment-variables.config @@ -56,3 +56,6 @@ option_settings: - namespace: aws:elasticbeanstalk:application:environment option_name: SCHEDULED_WORKER_SCHEDULE value: '*/5 * * * *' + - namespace: aws:elasticbeanstalk:application:environment + option_name: PORT + value: 3000 From 44da2c91fab03bebb162132f13bc4f9db72015f5 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 15:25:57 +0530 Subject: [PATCH 16/19] More env variables --- consumer/.ebextensions/01-environment-variables.config | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/consumer/.ebextensions/01-environment-variables.config b/consumer/.ebextensions/01-environment-variables.config index 6979473..9361caf 100644 --- a/consumer/.ebextensions/01-environment-variables.config +++ b/consumer/.ebextensions/01-environment-variables.config @@ -59,3 +59,12 @@ option_settings: - namespace: aws:elasticbeanstalk:application:environment option_name: PORT value: 3000 + - namespace: aws:elasticbeanstalk:application:environment + option_name: API_VERSION + value: v4 + - namespace: aws:elasticbeanstalk:application:environment + option_name: AUTH_SECRET + value: TBD + - namespace: aws:elasticbeanstalk:application:environment + option_name: VALID_ISSUERS + value: TBD From 5a55760976afecc45a67a28dc76f31320883154d Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 16:03:56 +0530 Subject: [PATCH 17/19] trying to fix path error --- consumer/src/worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consumer/src/worker.js b/consumer/src/worker.js index 3f869f7..306e962 100644 --- a/consumer/src/worker.js +++ b/consumer/src/worker.js @@ -158,7 +158,7 @@ if (!module.parent) { })(req, res, next); }); - app.post(`${config.apiVersion}/connect2sf/leadInfo`, asyncHandler(async (req, res, next) => { + app.post(`/${config.apiVersion}/connect2sf/leadInfo`, asyncHandler(async (req, res, next) => { const result = await LeadService.postLead(req.body); res.json(result); })); From 0adb03e44b11385d88a67c76ec2040e24135afb5 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 16:13:06 +0530 Subject: [PATCH 18/19] Fixing the argument name --- consumer/src/services/LeadService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/consumer/src/services/LeadService.js b/consumer/src/services/LeadService.js index 8997f7b..f7f048f 100644 --- a/consumer/src/services/LeadService.js +++ b/consumer/src/services/LeadService.js @@ -26,11 +26,12 @@ class LeadService { /** * Post the lead info to Salesforce - * @param {Object} reqBody Request body + * @param {Object} user Request body * @returns {Object} sample response */ - @logAndValidate(['reqBody'], postLeadSchema) + @logAndValidate(['user'], postLeadSchema) postLead(user) { // eslint-disable-line no-unused-vars + console.log(user, 'user'); let leadId = 0; return Promise.all([ ConfigurationService.getSalesforceCampaignId(), From a8d731d421b09377a8fbb8fc0c5264ed880c948f Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Thu, 27 Sep 2018 16:18:41 +0530 Subject: [PATCH 19/19] fixed typo --- consumer/src/services/LeadService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consumer/src/services/LeadService.js b/consumer/src/services/LeadService.js index f7f048f..5a50e1a 100644 --- a/consumer/src/services/LeadService.js +++ b/consumer/src/services/LeadService.js @@ -9,7 +9,7 @@ import ConfigurationService from './ConfigurationService'; import SalesforceService from './SalesforceService'; const postLeadSchema = Joi.object().keys({ - reqBody: Joi.object().keys({ + user: Joi.object().keys({ firstName: Joi.string().required(), lastName: Joi.string().required(), businessEmail: Joi.string().email().required(),