Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,16 @@ workflows:
filters:
branches:
only:
- develop
- recruit-apis
- feature/recommender-sync-develop
# This is alternate dev env for parallel testing
- "build-test":
context : org-global
filters:
branches:
only:
- gig-login-modal-update
only:
- free

# This is alternate dev env for parallel testing
- "build-qa":
context : org-global
Expand Down
124 changes: 103 additions & 21 deletions src/server/services/recruitCRM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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, '', {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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}`,
Expand All @@ -295,7 +371,9 @@ export default class RecruitCRMService {
file,
formHeaders,
errObj: await fileCandidateResponse.json(),
});
};
notifyKirilAndNick(error);
return res.send(error);
}
candidateData = await fileCandidateResponse.json();
}
Expand All @@ -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}`, {
Expand All @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions src/server/services/sendGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 5 additions & 1 deletion src/shared/components/Gigs/GigApply/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ export default function GigApply(props) {
{
application.error ? (
<React.Fragment>
<p styleName="error-text">{application.errorObj.message || JSON.stringify(application.errorObj)}</p>
{
application.errorObj ? (
<p styleName="error-text">{application.errorObj.message || JSON.stringify(application.errorObj)}</p>
) : null
}
<p>Looks like there is a problem on our end. Please try again.<br />If this persists please contact <a href="mailto:support@topcoder.com">support@topcoder.com</a>.</p>
<p>Please send us an email at <a href="mailto:gigwork@topcoder.com">gigwork@topcoder.com</a> with the subject ‘Gig Error’<br />and paste the URL for the gig you are attempting to apply for so that we know of your interest.</p>
</React.Fragment>
Expand Down
8 changes: 5 additions & 3 deletions src/shared/reducers/recruitCRM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down