From 66cb4185bd25a4b2b7d3cd63dcb7a0f1312cb190 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 5 Apr 2021 10:22:57 +0300 Subject: [PATCH 1/3] Implement #5453 --- src/server/services/recruitCRM.js | 124 +++++++++++++++--- src/server/services/sendGrid.js | 29 ++++ src/shared/components/Gigs/GigApply/index.jsx | 6 +- src/shared/reducers/recruitCRM.js | 8 +- 4 files changed, 142 insertions(+), 25 deletions(-) diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 4f8ee4e2e1..036271d110 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -5,10 +5,62 @@ import fetch from 'isomorphic-fetch'; import config from 'config'; import qs from 'qs'; import _ from 'lodash'; +import { logger } from 'topcoder-react-lib'; import GrowsurfService from './growsurf'; +import { sendEmailDirect } from './sendGrid'; const FormData = require('form-data'); +const JOB_FIELDS_RESPONSE = [ + 'id', + 'slug', + 'country', + 'locality', + 'city', + 'name', + 'custom_fields', + 'enable_job_application_form', + 'created_on', + 'updated_on', + 'min_annual_salary', + 'salary_type', + 'max_annual_salary', + 'job_description_text', +]; +const CANDIDATE_FIELDS_RESPONSE = [ + 'id', + 'slug', + 'first_name', + 'last_name', + 'email', + 'contact_number', + 'skill', + 'resume', + 'locality', + 'salary_expectation', + 'custom_fields', +]; + +/** + * Send email to Kiril/Nick for debuging gig application errors + * @param {Object} error the error + */ +function notifyKirilAndNick(error) { + logger.error(error); + sendEmailDirect({ + personalizations: [ + { + to: [{ email: 'kiril.kartunov@gmail.com' }, { email: 'ncastillo@topcoder.com' }], + subject: 'Gig application error alert', + }, + ], + from: { email: 'noreply@topcoder.com' }, + content: [{ + type: 'text/plain', value: `The error occured as JSON string:\n\n ${JSON.stringify(error)}`, + }], + }); +} + /** * Auxiliary class that handles communication with recruitCRM */ @@ -44,13 +96,17 @@ export default class RecruitCRMService { return this.getJobs(req, res, next); } if (response.status >= 400) { - return res.send({ + const error = { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, - }); + errObj: await response.json(), + }; + logger.error(error); + return res.send(error); } const data = await response.json(); + data.data = _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE)); return res.send(data); } catch (err) { return next(err); @@ -76,14 +132,17 @@ export default class RecruitCRMService { return this.getJob(req, res, next); } if (response.status >= 400) { - return res.send({ + const error = { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/${req.params.id}`, - }); + errObj: await response.json(), + }; + logger.error(error); + return res.send(error); } const data = await response.json(); - return res.send(data); + return res.send(_.pick(data, JOB_FIELDS_RESPONSE)); } catch (err) { return next(err); } @@ -108,11 +167,14 @@ export default class RecruitCRMService { return this.getJobs(req, res, next); } if (response.status >= 400) { - return res.send({ + const error = { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, - }); + errObj: await response.json(), + }; + logger.error(error); + return res.send(error); } const data = await response.json(); if (data.current_page < data.last_page) { @@ -133,13 +195,17 @@ export default class RecruitCRMService { const pageData = await pageDataRsp.json(); data.data = _.flatten(data.data.concat(pageData.data)); } - return res.send(data.data); + return res.send( + _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE)), + ); }) .catch(e => res.send({ error: e, })); } - return res.send(data.data); + return res.send( + _.map(data.data, j => _.pick(j, JOB_FIELDS_RESPONSE)), + ); } catch (err) { return next(err); } @@ -164,13 +230,17 @@ export default class RecruitCRMService { return this.searchCandidates(req, res, next); } if (response.status >= 400) { - return res.send({ + const error = { error: true, status: response.status, url: `${this.private.baseUrl}/v1/candidates/search?${qs.stringify(req.query)}`, - }); + errObj: await response.json(), + }; + logger.error(error); + return res.send(error); } const data = await response.json(); + data.data = _.map(data.data, j => _.pick(j, CANDIDATE_FIELDS_RESPONSE)); return res.send(data); } catch (err) { return next(err); @@ -215,6 +285,8 @@ export default class RecruitCRMService { form.custom_fields.push({ field_id: 6, value: `https://app.growsurf.com/dashboard/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${growRes.id}`, }); + } else { + notifyKirilAndNick(growRes); } // clear the cookie res.cookie(config.GROWSURF_COOKIE, '', { @@ -231,12 +303,14 @@ export default class RecruitCRMService { }, }); if (candidateResponse.status >= 300) { - return res.send({ + const error = { error: true, status: candidateResponse.status, url: `${this.private.baseUrl}/v1/candidates/search?email=${form.email}`, errObj: await candidateResponse.json(), - }); + }; + notifyKirilAndNick(error); + return res.send(error); } let candidateData = await candidateResponse.json(); if (candidateData.data) { @@ -265,13 +339,15 @@ export default class RecruitCRMService { body: JSON.stringify(form), }); if (workCandidateResponse.status >= 300) { - return res.send({ + const error = { error: true, status: workCandidateResponse.status, url: `${this.private.baseUrl}/v1/candidates${candidateSlug ? `/${candidateSlug}` : ''}`, form, errObj: await workCandidateResponse.json(), - }); + }; + notifyKirilAndNick(error); + return res.send(error); } candidateData = await workCandidateResponse.json(); // Attach resume to candidate if uploaded @@ -286,7 +362,7 @@ export default class RecruitCRMService { body: fileData, }); if (fileCandidateResponse.status >= 300) { - return res.send({ + const error = { error: true, status: fileCandidateResponse.status, url: `${this.private.baseUrl}/v1/candidates/${candidateData.slug}`, @@ -295,7 +371,9 @@ export default class RecruitCRMService { file, formHeaders, errObj: await fileCandidateResponse.json(), - }); + }; + notifyKirilAndNick(error); + return res.send(error); } candidateData = await fileCandidateResponse.json(); } @@ -314,14 +392,16 @@ export default class RecruitCRMService { success: true, }); } - return res.send({ + const error = { error: true, status: applyResponse.status, url: `${this.private.baseUrl}/v1/candidates/${candidateData.slug}/assign?job_slug=${id}`, form, candidateData, errObj, - }); + }; + notifyKirilAndNick(error); + return res.send(error); } // Set hired-stage const hireStageResponse = await fetch(`${this.private.baseUrl}/v1/candidates/${candidateData.slug}/hiring-stages/${id}`, { @@ -337,13 +417,15 @@ export default class RecruitCRMService { }), }); if (hireStageResponse.status >= 300) { - return res.send({ + const error = { error: true, status: hireStageResponse.status, url: `$${this.private.baseUrl}/v1/candidates/${candidateData.slug}/hiring-stages/${id}`, form, errObj: await hireStageResponse.json(), - }); + }; + notifyKirilAndNick(error); + return res.send(error); } // respond to API call const data = await applyResponse.json(); diff --git a/src/server/services/sendGrid.js b/src/server/services/sendGrid.js index ce8dda9ca2..9deb0378a3 100644 --- a/src/server/services/sendGrid.js +++ b/src/server/services/sendGrid.js @@ -38,4 +38,33 @@ export const sendEmail = async (req, res) => { } }; +/** + * Send email directly via the SendGrid API + * @param {Object} msg the payload + * @returns Promise + */ +export const sendEmailDirect = async (msg) => { + try { + const response = await fetch('https://api.sendgrid.com/v3/mail/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${config.SECRET.SENDGRID_API_KEY}`, + }, + body: JSON.stringify(msg), + }); + return response; + } catch (error) { + logger.error(error); + const { message, code, response } = error; + if (error.response) { + const { headers, body } = response; + return { + code, message, headers, body, + }; + } + return { message }; + } +}; + export default undefined; diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index d48f9ec819..a0b5ad7169 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -53,7 +53,11 @@ export default function GigApply(props) { { application.error ? ( -

{application.errorObj.message || JSON.stringify(application.errorObj)}

+ { + application.errorObj ? ( +

{application.errorObj.message || JSON.stringify(application.errorObj)}

+ ) : null + }

Looks like there is a problem on our end. Please try again.
If this persists please contact support@topcoder.com.

Please send us an email at gigwork@topcoder.com with the subject ‘Gig Error’
and paste the URL for the gig you are attempting to apply for so that we know of your interest.

diff --git a/src/shared/reducers/recruitCRM.js b/src/shared/reducers/recruitCRM.js index 0ff13090e3..4a7bbfa629 100644 --- a/src/shared/reducers/recruitCRM.js +++ b/src/shared/reducers/recruitCRM.js @@ -75,12 +75,14 @@ function onApplyForJobInit(state, { payload }) { * @param {Object} state Previous state. * @param {Object} action The action. */ -function onApplyForJobDone(state, { payload }) { +function onApplyForJobDone(state, action) { const r = { ...state, }; - r[payload.id].applying = false; - r[payload.id].application = payload.data; + if (!action.error) { + r[action.payload.id].applying = false; + r[action.payload.id].application = action.payload.data; + } return r; } From 79df3998f0bf3391f6ad9585dd0498ec66e7d05a Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 5 Apr 2021 14:29:41 +0300 Subject: [PATCH 2/3] ci: on test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d107a6dc6..f338940163 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -283,7 +283,7 @@ workflows: filters: branches: only: - - gig-application-update + - recruit-apis # This is alternate dev env for parallel testing - "build-qa": context : org-global From 74af18d6b63e255443c4cef2c212c426117506b8 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 6 Apr 2021 09:15:57 +0300 Subject: [PATCH 3/3] ci: on dev --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f338940163..fb8720eecf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -275,7 +275,7 @@ workflows: filters: branches: only: - - develop + - recruit-apis - feature/recommender-sync-develop # This is alternate dev env for parallel testing - "build-test": @@ -283,7 +283,7 @@ workflows: filters: branches: only: - - recruit-apis + - empty # This is alternate dev env for parallel testing - "build-qa": context : org-global