Skip to content

Commit c46c09c

Browse files
author
vikasrohit
authored
Merge pull request #642 from topcoder-platform/develop
Prod Release - BA details and return only Active BAs
2 parents 47a8549 + 8881f67 commit c46c09c

File tree

11 files changed

+285
-7
lines changed

11 files changed

+285
-7
lines changed

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ workflows:
152152
- UnitTests
153153
filters:
154154
branches:
155-
only: ['develop', 'connect-performance-testing']
155+
only: ['develop', 'connect-performance-testing', 'feature/get-markup-from-billing-account']
156156
- deployProd:
157157
context : org-global
158158
requires:
@@ -167,4 +167,4 @@ workflows:
167167
- deployProd
168168
- Connect-Performance-Testing:
169169
requires:
170-
- Hold [Performance-Testing]
170+
- Hold [Performance-Testing]

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ export const M2M_SCOPES = {
276276
WRITE: 'write:projects',
277277
READ_USER_BILLING_ACCOUNTS: 'read:user-billing-accounts',
278278
WRITE_PROJECTS_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
279+
READ_PROJECT_BILLING_ACCOUNT_DETAILS: 'read:project-billing-account-details',
279280
},
280281
PROJECT_MEMBERS: {
281282
ALL: 'all:project-members',

src/permissions/constants.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ const SCOPES_PROJECTS_WRITE = [
9696
*/
9797
const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [
9898
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
99-
M2M_SCOPES.READ_USER_BILLING_ACCOUNTS,
99+
M2M_SCOPES.PROJECTS.READ_USER_BILLING_ACCOUNTS,
100+
];
101+
102+
/**
103+
* M2M scopes to "read" available Billing Accounts for the project
104+
*/
105+
const SCOPES_PROJECTS_READ_BILLING_ACCOUNT_DETAILS = [
106+
M2M_SCOPES.PROJECTS.READ_PROJECT_BILLING_ACCOUNT_DETAILS,
100107
];
101108

102109
/**
@@ -277,6 +284,18 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
277284
scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS,
278285
},
279286

287+
/*
288+
* Project Invite
289+
*/
290+
READ_PROJECT_BILLING_ACCOUNT_DETAILS: {
291+
meta: {
292+
title: 'Read details of billing accounts - only allowed to m2m calls',
293+
group: 'Project Billing Accounts',
294+
description: 'Who can view the details of the Billing Account attached to the project',
295+
},
296+
scopes: SCOPES_PROJECTS_READ_BILLING_ACCOUNT_DETAILS,
297+
},
298+
280299
/*
281300
* Project Member
282301
*/

src/permissions/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module.exports = () => {
2424
PERMISSION.READ_AVL_PROJECT_BILLING_ACCOUNTS,
2525
]));
2626

27+
Authorizer.setPolicy('projectBillingAccount.view', generalPermission([
28+
PERMISSION.READ_PROJECT_BILLING_ACCOUNT_DETAILS,
29+
]));
30+
2731
Authorizer.setPolicy('projectMember.create', generalPermission([
2832
PERMISSION.CREATE_PROJECT_MEMBER_OWN,
2933
PERMISSION.CREATE_PROJECT_MEMBER_NOT_OWN,

src/routes/billingAccounts/get.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import _ from 'lodash';
2+
import validate from 'express-validation';
3+
import Joi from 'joi';
4+
import { middleware as tcMiddleware } from 'tc-core-library-js';
5+
import SalesforceService from '../../services/salesforceService';
6+
import models from '../../models';
7+
8+
/**
9+
* API to get project attachments.
10+
*
11+
*/
12+
13+
const permissions = tcMiddleware.permissions;
14+
15+
const schema = {
16+
params: {
17+
projectId: Joi.number().integer().positive().required(),
18+
},
19+
};
20+
21+
module.exports = [
22+
validate(schema),
23+
permissions('projectBillingAccount.view'),
24+
async (req, res, next) => {
25+
const projectId = _.parseInt(req.params.projectId);
26+
try {
27+
const project = await models.Project.findOne({
28+
where: { id: projectId },
29+
attributes: ['id', 'billingAccountId'],
30+
raw: true,
31+
});
32+
if (!project) {
33+
const err = new Error(`Project with id "${projectId}" not found`);
34+
err.status = 404;
35+
throw err;
36+
}
37+
const billingAccountId = project.billingAccountId;
38+
if (!billingAccountId) {
39+
const err = new Error('Billing Account not found');
40+
err.status = 404;
41+
throw err;
42+
}
43+
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
44+
// eslint-disable-next-line
45+
const sql = `SELECT TopCoder_Billing_Account_Id__c, Mark_Up__c from Topcoder_Billing_Account__c tba where TopCoder_Billing_Account_Id__c='${billingAccountId}'`;
46+
req.log.debug(sql);
47+
const billingAccount = await SalesforceService.queryBillingAccount(sql, accessToken, instanceUrl, req.log);
48+
res.json(billingAccount);
49+
} catch (error) {
50+
req.log.error(error);
51+
next(error);
52+
}
53+
},
54+
];
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/* eslint-disable no-unused-expressions */
2+
import chai from 'chai';
3+
import request from 'supertest';
4+
import sinon from 'sinon';
5+
6+
import models from '../../models';
7+
import server from '../../app';
8+
import testUtil from '../../tests/util';
9+
import SalesforceService from '../../services/salesforceService';
10+
11+
chai.should();
12+
13+
// demo data which might be returned by the `SalesforceService.query`
14+
const billingAccountData = {
15+
tcBillingAccountId: 123123,
16+
markup: 50,
17+
};
18+
19+
describe('Project Billing Accounts list', () => {
20+
let project1;
21+
let project2;
22+
let salesforceAuthenticate;
23+
let salesforceQuery;
24+
25+
beforeEach((done) => {
26+
testUtil.clearDb()
27+
.then(() => testUtil.clearES())
28+
.then(() => models.Project.create({
29+
type: 'generic',
30+
directProjectId: 1,
31+
billingAccountId: 1,
32+
name: 'test1',
33+
description: 'test project1',
34+
status: 'draft',
35+
details: {},
36+
createdBy: 1,
37+
updatedBy: 1,
38+
lastActivityAt: 1,
39+
lastActivityUserId: '1',
40+
}).then((p) => {
41+
project1 = p;
42+
// create members
43+
return models.ProjectMember.create({
44+
userId: testUtil.userIds.copilot,
45+
projectId: project1.id,
46+
role: 'copilot',
47+
isPrimary: true,
48+
createdBy: 1,
49+
updatedBy: 1,
50+
}).then(() => models.ProjectMember.create({
51+
userId: testUtil.userIds.member,
52+
projectId: project1.id,
53+
role: 'customer',
54+
isPrimary: false,
55+
createdBy: 1,
56+
updatedBy: 1,
57+
}));
58+
})).then(() => models.Project.create({
59+
type: 'generic',
60+
directProjectId: 1,
61+
billingAccountId: null, // do not define billingAccountId
62+
name: 'test1',
63+
description: 'test project1',
64+
status: 'draft',
65+
details: {},
66+
createdBy: 1,
67+
updatedBy: 1,
68+
lastActivityAt: 1,
69+
lastActivityUserId: '1',
70+
}).then((p) => {
71+
project2 = p;
72+
// create members
73+
return models.ProjectMember.create({
74+
userId: testUtil.userIds.copilot,
75+
projectId: project2.id,
76+
role: 'copilot',
77+
isPrimary: true,
78+
createdBy: 1,
79+
updatedBy: 1,
80+
}).then(() => models.ProjectMember.create({
81+
userId: testUtil.userIds.member,
82+
projectId: project2.id,
83+
role: 'customer',
84+
isPrimary: false,
85+
createdBy: 1,
86+
updatedBy: 1,
87+
}));
88+
}))
89+
.then(() => {
90+
salesforceAuthenticate = sinon.stub(SalesforceService, 'authenticate', () => Promise.resolve({
91+
accessToken: 'mock',
92+
instanceUrl: 'mock_url',
93+
}));
94+
// eslint-disable-next-line
95+
salesforceQuery = sinon.stub(SalesforceService, 'queryBillingAccount', () => Promise.resolve(billingAccountData));
96+
done();
97+
});
98+
});
99+
100+
afterEach((done) => {
101+
salesforceAuthenticate.restore();
102+
salesforceQuery.restore();
103+
done();
104+
});
105+
106+
after((done) => {
107+
testUtil.clearDb(done);
108+
});
109+
110+
describe('Get /projects/{id}/billingAccounts', () => {
111+
it('should return 403 for anonymous user', (done) => {
112+
request(server)
113+
.get(`/v5/projects/${project1.id}/billingAccount`)
114+
.expect(403, done);
115+
});
116+
117+
it('should return 403 for admin', (done) => {
118+
request(server)
119+
.get(`/v5/projects/${project1.id}/billingAccount`)
120+
.set({
121+
Authorization: `Bearer ${testUtil.jwts.admin}`,
122+
})
123+
.send()
124+
.expect(403, done);
125+
});
126+
127+
it('should return 404 if the project is not found', (done) => {
128+
request(server)
129+
.get('/v5/projects/11223344/billingAccount')
130+
.set({
131+
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
132+
})
133+
.send()
134+
.expect(404, done);
135+
});
136+
137+
it('should return 404 if billing account is not defined in the project', (done) => {
138+
request(server)
139+
.get(`/v5/projects/${project2.id}/billingAccount`)
140+
.set({
141+
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
142+
})
143+
.send()
144+
.expect(404, done);
145+
});
146+
147+
it('should return billing account details using M2M token with "read:project-billing-account-details" scope',
148+
(done) => {
149+
request(server)
150+
.get(`/v5/projects/${project1.id}/billingAccount`)
151+
.set({
152+
Authorization: `Bearer ${testUtil.m2m['read:project-billing-account-details']}`,
153+
})
154+
.send()
155+
.expect(200)
156+
.end((err, res) => {
157+
if (err) {
158+
done(err);
159+
} else {
160+
const resJson = res.body;
161+
resJson.should.deep.equal(billingAccountData);
162+
done();
163+
}
164+
});
165+
});
166+
});
167+
});

src/routes/billingAccounts/list.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ module.exports = [
2626
try {
2727
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
2828
// eslint-disable-next-line
29-
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}'`;
29+
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}'`;
3030
// and Topcoder_Billing_Account__r.TC_Connect_Project_ID__c='${projectId}'
3131
req.log.debug(sql);
32-
const billingAccounts = await SalesforceService.query(sql, accessToken, instanceUrl, req.log);
32+
const billingAccounts = await SalesforceService.queryUserBillingAccounts(sql, accessToken, instanceUrl, req.log);
3333
res.json(billingAccounts);
3434
} catch (error) {
3535
req.log.error(error);

src/routes/billingAccounts/list.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ describe('Project Billing Accounts list', () => {
7171
accessToken: 'mock',
7272
instanceUrl: 'mock_url',
7373
}));
74-
salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve(billingAccountsData));
74+
// eslint-disable-next-line
75+
salesforceQuery = sinon.stub(SalesforceService, 'queryUserBillingAccounts', () => Promise.resolve(billingAccountsData));
7576
done();
7677
});
7778
});

src/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
123123

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

127129
router.route('/v5/projects/:projectId(\\d+)/members')
128130
.get(require('./projectMembers/list'))

src/services/salesforceService.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class SalesforceService {
5454
* @param {Object} logger logger to be used for logging
5555
* @returns {{totalSize: Number, done: Boolean, records: Array}} the result
5656
*/
57-
static query(sql, accessToken, instanceUrl, logger) {
57+
static queryUserBillingAccounts(sql, accessToken, instanceUrl, logger) {
5858
return axios({
5959
url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`,
6060
method: 'get',
@@ -77,6 +77,35 @@ class SalesforceService {
7777
return billingAccounts;
7878
});
7979
}
80+
81+
/**
82+
* Run the query statement
83+
* @param {String} sql the Saleforce sql statement
84+
* @param {String} accessToken the access token
85+
* @param {String} instanceUrl the salesforce instance url
86+
* @param {Object} logger logger to be used for logging
87+
* @returns {{totalSize: Number, done: Boolean, records: Array}} the result
88+
*/
89+
static queryBillingAccount(sql, accessToken, instanceUrl, logger) {
90+
return axios({
91+
url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`,
92+
method: 'get',
93+
headers: { authorization: `Bearer ${accessToken}` },
94+
}).then((res) => {
95+
if (logger) {
96+
logger.debug(_.get(res, 'data.records', []));
97+
}
98+
const billingAccounts = _.get(res, 'data.records', []).map(o => ({
99+
tcBillingAccountId: util.parseIntStrictly(
100+
_.get(o, 'TopCoder_Billing_Account_Id__c'),
101+
10,
102+
null, // fallback to null if cannot parse
103+
),
104+
markup: _.get(o, 'Mark_Up__c'),
105+
}));
106+
return billingAccounts.length > 0 ? billingAccounts[0] : {};
107+
});
108+
}
80109
}
81110

82111
export default SalesforceService;

src/tests/util.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)