Skip to content

Feature/get markup from billing account #637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 11, 2021
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
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ workflows:
- UnitTests
filters:
branches:
only: ['develop', 'connect-performance-testing']
only: ['develop', 'connect-performance-testing', 'feature/get-markup-from-billing-account']
- deployProd:
context : org-global
requires:
Expand All @@ -167,4 +167,4 @@ workflows:
- deployProd
- Connect-Performance-Testing:
requires:
- Hold [Performance-Testing]
- Hold [Performance-Testing]
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export const M2M_SCOPES = {
WRITE: 'write:projects',
READ_USER_BILLING_ACCOUNTS: 'read:user-billing-accounts',
WRITE_PROJECTS_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
READ_PROJECT_BILLING_ACCOUNT_DETAILS: 'read:project-billing-account-details',
},
PROJECT_MEMBERS: {
ALL: 'all:project-members',
Expand Down
21 changes: 20 additions & 1 deletion src/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ const SCOPES_PROJECTS_WRITE = [
*/
const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
M2M_SCOPES.READ_USER_BILLING_ACCOUNTS,
M2M_SCOPES.PROJECTS.READ_USER_BILLING_ACCOUNTS,
];

/**
* M2M scopes to "read" available Billing Accounts for the project
*/
const SCOPES_PROJECTS_READ_BILLING_ACCOUNT_DETAILS = [
M2M_SCOPES.PROJECTS.READ_PROJECT_BILLING_ACCOUNT_DETAILS,
];

/**
Expand Down Expand Up @@ -277,6 +284,18 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS,
},

/*
* Project Invite
*/
READ_PROJECT_BILLING_ACCOUNT_DETAILS: {
meta: {
title: 'Read details of billing accounts - only allowed to m2m calls',
group: 'Project Billing Accounts',
description: 'Who can view the details of the Billing Account attached to the project',
},
scopes: SCOPES_PROJECTS_READ_BILLING_ACCOUNT_DETAILS,
},

/*
* Project Member
*/
Expand Down
4 changes: 4 additions & 0 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ module.exports = () => {
PERMISSION.READ_AVL_PROJECT_BILLING_ACCOUNTS,
]));

Authorizer.setPolicy('projectBillingAccount.view', generalPermission([
PERMISSION.READ_PROJECT_BILLING_ACCOUNT_DETAILS,
]));

Authorizer.setPolicy('projectMember.create', generalPermission([
PERMISSION.CREATE_PROJECT_MEMBER_OWN,
PERMISSION.CREATE_PROJECT_MEMBER_NOT_OWN,
Expand Down
54 changes: 54 additions & 0 deletions src/routes/billingAccounts/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import _ from 'lodash';
import validate from 'express-validation';
import Joi from 'joi';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import SalesforceService from '../../services/salesforceService';
import models from '../../models';

/**
* API to get project attachments.
*
*/

const permissions = tcMiddleware.permissions;

const schema = {
params: {
projectId: Joi.number().integer().positive().required(),
},
};

module.exports = [
validate(schema),
permissions('projectBillingAccount.view'),
async (req, res, next) => {
const projectId = _.parseInt(req.params.projectId);
try {
const project = await models.Project.findOne({
where: { id: projectId },
attributes: ['id', 'billingAccountId'],
raw: true,
});
if (!project) {
const err = new Error(`Project with id "${projectId}" not found`);
err.status = 404;
throw err;
}
const billingAccountId = project.billingAccountId;
if (!billingAccountId) {
const err = new Error('Billing Account not found');
err.status = 404;
throw err;
}
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
// eslint-disable-next-line
const sql = `SELECT TopCoder_Billing_Account_Id__c, Mark_Up__c from Topcoder_Billing_Account__c tba where TopCoder_Billing_Account_Id__c='${billingAccountId}'`;
req.log.debug(sql);
const billingAccount = await SalesforceService.queryBillingAccount(sql, accessToken, instanceUrl, req.log);
res.json(billingAccount);
} catch (error) {
req.log.error(error);
next(error);
}
},
];
167 changes: 167 additions & 0 deletions src/routes/billingAccounts/get.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable no-unused-expressions */
import chai from 'chai';
import request from 'supertest';
import sinon from 'sinon';

import models from '../../models';
import server from '../../app';
import testUtil from '../../tests/util';
import SalesforceService from '../../services/salesforceService';

chai.should();

// demo data which might be returned by the `SalesforceService.query`
const billingAccountData = {
tcBillingAccountId: 123123,
markup: 50,
};

describe('Project Billing Accounts list', () => {
let project1;
let project2;
let salesforceAuthenticate;
let salesforceQuery;

beforeEach((done) => {
testUtil.clearDb()
.then(() => testUtil.clearES())
.then(() => models.Project.create({
type: 'generic',
directProjectId: 1,
billingAccountId: 1,
name: 'test1',
description: 'test project1',
status: 'draft',
details: {},
createdBy: 1,
updatedBy: 1,
lastActivityAt: 1,
lastActivityUserId: '1',
}).then((p) => {
project1 = p;
// create members
return models.ProjectMember.create({
userId: testUtil.userIds.copilot,
projectId: project1.id,
role: 'copilot',
isPrimary: true,
createdBy: 1,
updatedBy: 1,
}).then(() => models.ProjectMember.create({
userId: testUtil.userIds.member,
projectId: project1.id,
role: 'customer',
isPrimary: false,
createdBy: 1,
updatedBy: 1,
}));
})).then(() => models.Project.create({
type: 'generic',
directProjectId: 1,
billingAccountId: null, // do not define billingAccountId
name: 'test1',
description: 'test project1',
status: 'draft',
details: {},
createdBy: 1,
updatedBy: 1,
lastActivityAt: 1,
lastActivityUserId: '1',
}).then((p) => {
project2 = p;
// create members
return models.ProjectMember.create({
userId: testUtil.userIds.copilot,
projectId: project2.id,
role: 'copilot',
isPrimary: true,
createdBy: 1,
updatedBy: 1,
}).then(() => models.ProjectMember.create({
userId: testUtil.userIds.member,
projectId: project2.id,
role: 'customer',
isPrimary: false,
createdBy: 1,
updatedBy: 1,
}));
}))
.then(() => {
salesforceAuthenticate = sinon.stub(SalesforceService, 'authenticate', () => Promise.resolve({
accessToken: 'mock',
instanceUrl: 'mock_url',
}));
// eslint-disable-next-line
salesforceQuery = sinon.stub(SalesforceService, 'queryBillingAccount', () => Promise.resolve(billingAccountData));
done();
});
});

afterEach((done) => {
salesforceAuthenticate.restore();
salesforceQuery.restore();
done();
});

after((done) => {
testUtil.clearDb(done);
});

describe('Get /projects/{id}/billingAccounts', () => {
it('should return 403 for anonymous user', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccount`)
.expect(403, done);
});

it('should return 403 for admin', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccount`)
.set({
Authorization: `Bearer ${testUtil.jwts.admin}`,
})
.send()
.expect(403, done);
});

it('should return 404 if the project is not found', (done) => {
request(server)
.get('/v5/projects/11223344/billingAccount')
.set({
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
})
.send()
.expect(404, done);
});

it('should return 404 if billing account is not defined in the project', (done) => {
request(server)
.get(`/v5/projects/${project2.id}/billingAccount`)
.set({
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
})
.send()
.expect(404, done);
});

it('should return billing account details using M2M token with "read:project-billing-account-details" scope',
(done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccount`)
.set({
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
})
.send()
.expect(200)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body;
resJson.should.deep.equal(billingAccountData);
done();
}
});
});
});
});
4 changes: 2 additions & 2 deletions src/routes/billingAccounts/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ module.exports = [
try {
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
// eslint-disable-next-line
const sql = `SELECT Topcoder_Billing_Account__r.id, Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c, Topcoder_Billing_Account__r.Billing_Account_Name__c, Topcoder_Billing_Account__r.Start_Date__c, Topcoder_Billing_Account__r.End_Date__c from Topcoder_Billing_Account_Resource__c tbar where UserID__c='${userId}'`;
const sql = `SELECT Topcoder_Billing_Account__r.id, Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c, Topcoder_Billing_Account__r.Billing_Account_Name__c, Topcoder_Billing_Account__r.Start_Date__c, Topcoder_Billing_Account__r.End_Date__c from Topcoder_Billing_Account_Resource__c tbar where Topcoder_Billing_Account__r.Active__c=true AND UserID__c='${userId}'`;
// and Topcoder_Billing_Account__r.TC_Connect_Project_ID__c='${projectId}'
req.log.debug(sql);
const billingAccounts = await SalesforceService.query(sql, accessToken, instanceUrl, req.log);
const billingAccounts = await SalesforceService.queryUserBillingAccounts(sql, accessToken, instanceUrl, req.log);
res.json(billingAccounts);
} catch (error) {
req.log.error(error);
Expand Down
3 changes: 2 additions & 1 deletion src/routes/billingAccounts/list.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ describe('Project Billing Accounts list', () => {
accessToken: 'mock',
instanceUrl: 'mock_url',
}));
salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve(billingAccountsData));
// eslint-disable-next-line
salesforceQuery = sinon.stub(SalesforceService, 'queryUserBillingAccounts', () => Promise.resolve(billingAccountsData));
done();
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)

router.route('/v5/projects/:projectId(\\d+)/billingAccounts')
.get(require('./billingAccounts/list'));
router.route('/v5/projects/:projectId(\\d+)/billingAccount')
.get(require('./billingAccounts/get'));

router.route('/v5/projects/:projectId(\\d+)/members')
.get(require('./projectMembers/list'))
Expand Down
31 changes: 30 additions & 1 deletion src/services/salesforceService.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class SalesforceService {
* @param {Object} logger logger to be used for logging
* @returns {{totalSize: Number, done: Boolean, records: Array}} the result
*/
static query(sql, accessToken, instanceUrl, logger) {
static queryUserBillingAccounts(sql, accessToken, instanceUrl, logger) {
return axios({
url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`,
method: 'get',
Expand All @@ -77,6 +77,35 @@ class SalesforceService {
return billingAccounts;
});
}

/**
* Run the query statement
* @param {String} sql the Saleforce sql statement
* @param {String} accessToken the access token
* @param {String} instanceUrl the salesforce instance url
* @param {Object} logger logger to be used for logging
* @returns {{totalSize: Number, done: Boolean, records: Array}} the result
*/
static queryBillingAccount(sql, accessToken, instanceUrl, logger) {
return axios({
url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`,
method: 'get',
headers: { authorization: `Bearer ${accessToken}` },
}).then((res) => {
if (logger) {
logger.debug(_.get(res, 'data.records', []));
}
const billingAccounts = _.get(res, 'data.records', []).map(o => ({
tcBillingAccountId: util.parseIntStrictly(
_.get(o, 'TopCoder_Billing_Account_Id__c'),
10,
null, // fallback to null if cannot parse
),
markup: _.get(o, 'Mark_Up__c'),
}));
return billingAccounts.length > 0 ? billingAccounts[0] : {};
});
}
}

export default SalesforceService;
1 change: 1 addition & 0 deletions src/tests/util.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.