Skip to content

Commit 5612ded

Browse files
author
vikasrohit
authored
Merge pull request #621 from topcoder-platform/feature/sfdc-lookup-for-billing-accounts
[DEV] New API endpoint to get Billing Accounts from SFDC
2 parents 8b17f53 + f986af1 commit 5612ded

File tree

10 files changed

+351
-6
lines changed

10 files changed

+351
-6
lines changed

config/custom-environment-variables.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,11 @@
7070
"EMBED_REPORTS_MAPPING": "EMBED_REPORTS_MAPPING",
7171
"ALLOWED_USERS": "REPORTS_ALLOWED_USERS"
7272
},
73-
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID"
73+
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID",
74+
"salesforce": {
75+
"CLIENT_AUDIENCE": "SALESFORCE_AUDIENCE",
76+
"CLIENT_KEY": "SALESFORCE_CLIENT_KEY",
77+
"SUBJECT": "SALESFORCE_SUBJECT",
78+
"CLIENT_ID": "SALESFORCE_CLIENT_ID"
79+
}
7480
}

config/default.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,11 @@
7676
"ALLOWED_USERS": "[]"
7777
},
7878
"DEFAULT_M2M_USERID": -101,
79-
"taasJobApiUrl": "https://api.topcoder.com/v5/jobs"
79+
"taasJobApiUrl": "https://api.topcoder.com/v5/jobs",
80+
"salesforce": {
81+
"CLIENT_KEY": "",
82+
"CLIENT_AUDIENCE": "",
83+
"SUBJECT": "",
84+
"CLIENT_ID": ""
85+
}
8086
}

src/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@ export const M2M_SCOPES = {
274274
ALL: 'all:projects',
275275
READ: 'read:projects',
276276
WRITE: 'write:projects',
277-
WRITE_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
277+
READ_USER_BILLING_ACCOUNTS: 'read:user-billing-accounts',
278+
WRITE_PROJECTS_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
278279
},
279280
PROJECT_MEMBERS: {
280281
ALL: 'all:project-members',

src/permissions/constants.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,20 @@ const SCOPES_PROJECTS_WRITE = [
9191
M2M_SCOPES.PROJECTS.WRITE,
9292
];
9393

94+
/**
95+
* M2M scopes to "read" available Billing Accounts for the project
96+
*/
97+
const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [
98+
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
99+
M2M_SCOPES.READ_USER_BILLING_ACCOUNTS,
100+
];
101+
94102
/**
95103
* M2M scopes to "write" billingAccountId property
96104
*/
97-
const SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS = [
105+
const SCOPES_PROJECTS_WRITE_PROJECTS_BILLING_ACCOUNTS = [
98106
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
99-
M2M_SCOPES.PROJECTS.WRITE_BILLING_ACCOUNTS,
107+
M2M_SCOPES.PROJECTS.WRITE_PROJECTS_BILLING_ACCOUNTS,
100108
];
101109

102110
/**
@@ -231,7 +239,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
231239
USER_ROLE.MANAGER,
232240
USER_ROLE.TOPCODER_ADMIN,
233241
],
234-
scopes: SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS,
242+
scopes: SCOPES_PROJECTS_WRITE_PROJECTS_BILLING_ACCOUNTS,
235243
},
236244

237245
DELETE_PROJECT: {
@@ -252,6 +260,23 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
252260
scopes: SCOPES_PROJECTS_WRITE,
253261
},
254262

263+
/*
264+
* Project Invite
265+
*/
266+
READ_AVL_PROJECT_BILLING_ACCOUNTS: {
267+
meta: {
268+
title: 'Read Available Project Billing Accounts',
269+
group: 'Project Billing Accounts',
270+
description: 'Who can view the Billing Accounts available for the project',
271+
},
272+
projectRoles: [
273+
...PROJECT_ROLES_MANAGEMENT,
274+
PROJECT_MEMBER_ROLE.COPILOT,
275+
],
276+
topcoderRoles: TOPCODER_ROLES_ADMINS,
277+
scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS,
278+
},
279+
255280
/*
256281
* Project Member
257282
*/

src/permissions/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ module.exports = () => {
2020
Authorizer.setPolicy('project.edit', generalPermission(PERMISSION.UPDATE_PROJECT));
2121
Authorizer.setPolicy('project.delete', generalPermission(PERMISSION.DELETE_PROJECT));
2222

23+
Authorizer.setPolicy('projectBillingAccounts.view', generalPermission([
24+
PERMISSION.READ_AVL_PROJECT_BILLING_ACCOUNTS,
25+
]));
26+
2327
Authorizer.setPolicy('projectMember.create', generalPermission([
2428
PERMISSION.CREATE_PROJECT_MEMBER_OWN,
2529
PERMISSION.CREATE_PROJECT_MEMBER_NOT_OWN,

src/routes/billingAccounts/list.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
7+
/**
8+
* API to get project attachments.
9+
*
10+
*/
11+
12+
const permissions = tcMiddleware.permissions;
13+
14+
const schema = {
15+
params: {
16+
projectId: Joi.number().integer().positive().required(),
17+
},
18+
};
19+
20+
module.exports = [
21+
validate(schema),
22+
permissions('projectBillingAccounts.view'),
23+
async (req, res, next) => {
24+
// const projectId = _.parseInt(req.params.projectId);
25+
const userId = req.authUser.userId;
26+
try {
27+
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
28+
// 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}'`;
30+
// and Topcoder_Billing_Account__r.TC_Connect_Project_ID__c='${projectId}'
31+
req.log.debug(sql);
32+
const billingAccounts = await SalesforceService.query(sql, accessToken, instanceUrl, req.log);
33+
res.json(billingAccounts);
34+
} catch (error) {
35+
req.log.error(error);
36+
next(error);
37+
}
38+
},
39+
];
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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 billingAccountsData = [
15+
{
16+
sfBillingAccountId: 123,
17+
tcBillingAccountId: 123123,
18+
name: 'Billing Account 1',
19+
startDate: '2021-02-10T18:51:27Z',
20+
endDate: '2021-03-10T18:51:27Z',
21+
}, {
22+
sfBillingAccountId: 456,
23+
tcBillingAccountId: 456456,
24+
name: 'Billing Account 2',
25+
startDate: '2011-02-10T18:51:27Z',
26+
endDate: '2011-03-10T18:51:27Z',
27+
},
28+
];
29+
30+
describe('Project Billing Accounts list', () => {
31+
let project1;
32+
let salesforceAuthenticate;
33+
let salesforceQuery;
34+
35+
beforeEach((done) => {
36+
testUtil.clearDb()
37+
.then(() => testUtil.clearES())
38+
.then(() => {
39+
models.Project.create({
40+
type: 'generic',
41+
directProjectId: 1,
42+
billingAccountId: 1,
43+
name: 'test1',
44+
description: 'test project1',
45+
status: 'draft',
46+
details: {},
47+
createdBy: 1,
48+
updatedBy: 1,
49+
lastActivityAt: 1,
50+
lastActivityUserId: '1',
51+
}).then((p) => {
52+
project1 = p;
53+
// create members
54+
return models.ProjectMember.create({
55+
userId: testUtil.userIds.copilot,
56+
projectId: project1.id,
57+
role: 'copilot',
58+
isPrimary: true,
59+
createdBy: 1,
60+
updatedBy: 1,
61+
}).then(() => models.ProjectMember.create({
62+
userId: testUtil.userIds.member,
63+
projectId: project1.id,
64+
role: 'customer',
65+
isPrimary: false,
66+
createdBy: 1,
67+
updatedBy: 1,
68+
}));
69+
}).then(() => {
70+
salesforceAuthenticate = sinon.stub(SalesforceService, 'authenticate', () => Promise.resolve({
71+
accessToken: 'mock',
72+
instanceUrl: 'mock_url',
73+
}));
74+
salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve(billingAccountsData));
75+
done();
76+
});
77+
});
78+
});
79+
80+
afterEach((done) => {
81+
salesforceAuthenticate.restore();
82+
salesforceQuery.restore();
83+
done();
84+
});
85+
86+
after((done) => {
87+
testUtil.clearDb(done);
88+
});
89+
90+
describe('List /projects/{id}/billingAccounts', () => {
91+
it('should return 403 for anonymous user', (done) => {
92+
request(server)
93+
.get(`/v5/projects/${project1.id}/billingAccounts`)
94+
.expect(403, done);
95+
});
96+
97+
it('should return 403 for a customer user who is a member of the project', (done) => {
98+
request(server)
99+
.get(`/v5/projects/${project1.id}/billingAccounts`)
100+
.set({
101+
Authorization: `Bearer ${testUtil.jwts.member}`,
102+
})
103+
.send()
104+
.expect(403, done);
105+
});
106+
107+
it('should return 403 for a topcoder user who is not a member of the project', (done) => {
108+
request(server)
109+
.get(`/v5/projects/${project1.id}/billingAccounts`)
110+
.set({
111+
Authorization: `Bearer ${testUtil.jwts.copilotManager}`,
112+
})
113+
.send()
114+
.expect(403, done);
115+
});
116+
117+
it('should return all billing accounts for a topcoder user who is a member of the project', (done) => {
118+
request(server)
119+
.get(`/v5/projects/${project1.id}/billingAccounts`)
120+
.set({
121+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
122+
})
123+
.send()
124+
.expect(200)
125+
.end((err, res) => {
126+
if (err) {
127+
done(err);
128+
} else {
129+
const resJson = res.body;
130+
resJson.should.have.length(2);
131+
resJson.should.include(billingAccountsData[0]);
132+
resJson.should.include(billingAccountsData[1]);
133+
done();
134+
}
135+
});
136+
});
137+
138+
it('should return all billing accounts to admin', (done) => {
139+
request(server)
140+
.get(`/v5/projects/${project1.id}/billingAccounts`)
141+
.set({
142+
Authorization: `Bearer ${testUtil.jwts.admin}`,
143+
})
144+
.send()
145+
.expect(200)
146+
.end((err, res) => {
147+
if (err) {
148+
done(err);
149+
} else {
150+
const resJson = res.body;
151+
resJson.should.have.length(2);
152+
resJson.should.have.length(2);
153+
resJson.should.include(billingAccountsData[0]);
154+
resJson.should.include(billingAccountsData[1]);
155+
done();
156+
}
157+
});
158+
});
159+
160+
it('should return all billing accounts using M2M token with "read:user-billing-accounts" scope', (done) => {
161+
request(server)
162+
.get(`/v5/projects/${project1.id}/billingAccounts`)
163+
.set({
164+
Authorization: `Bearer ${testUtil.m2m['read:user-billing-accounts']}`,
165+
})
166+
.send()
167+
.expect(200)
168+
.end((err, res) => {
169+
if (err) {
170+
done(err);
171+
} else {
172+
const resJson = res.body;
173+
resJson.should.have.length(2);
174+
resJson.should.have.length(2);
175+
resJson.should.include(billingAccountsData[0]);
176+
resJson.should.include(billingAccountsData[1]);
177+
done();
178+
}
179+
});
180+
});
181+
});
182+
});

src/routes/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
121121
.patch(require('./scopeChangeRequests/update'));
122122
// .delete(require('./scopeChangeRequests/delete'));
123123

124+
router.route('/v5/projects/:projectId(\\d+)/billingAccounts')
125+
.get(require('./billingAccounts/list'));
126+
124127
router.route('/v5/projects/:projectId(\\d+)/members')
125128
.get(require('./projectMembers/list'))
126129
.post(require('./projectMembers/create'));

0 commit comments

Comments
 (0)