Skip to content

Commit 848b3bf

Browse files
authored
Merge pull request #9 from topcoder-platform/PM-3575_salesforce-user-ba
PM-3575 - Add new endpoint to fetch salesforce BAs for user
2 parents cfd4a7e + cc80754 commit 848b3bf

8 files changed

Lines changed: 238 additions & 7 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,15 @@ AUTH0_AUDIENCE="" # e.g. https://m2m.topcoder-dev.com/
1111
AUTH0_ISSUER="" # e.g. https://topcoder-dev.auth0.com/
1212
ALLOWED_ROLES="Administrator,Topcoder User,Manager" # comma-separated
1313

14+
# Salesforce
15+
SALESFORCE_CLIENT_ID=""
16+
SALESFORCE_SUBJECT="" # e.g. integration@topcoder.com
17+
SALESFORCE_CLIENT_KEY="" # PEM private key (use `\\n` for newlines)
18+
SALESFORCE_AUDIENCE="https://login.salesforce.com" # or https://test.salesforce.com for sandbox
19+
20+
# Optional SFDC field mapping overrides (defaults used by service)
21+
SFDC_BILLING_ACCOUNT_NAME_FIELD="Billing_Account_name__c"
22+
SFDC_BILLING_ACCOUNT_MARKUP_FIELD="Mark_Up__c"
23+
SFDC_BILLING_ACCOUNT_ACTIVE_FIELD="Active__c"
1424
# Service
1525
PORT=3000

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- `GET /billing-accounts`
99
- `POST /billing-accounts`
1010
- `GET /billing-accounts/:billingAccountId` (includes locked/consumed arrays + budget totals)
11+
- `GET /billing-accounts/users/:userId` (list billing accounts accessible by the given Topcoder user ID — resolved via Salesforce resource object)
1112
- `PATCH /billing-accounts/:billingAccountId`
1213
- `PATCH /billing-accounts/:billingAccountId/lock-amount` (0 amount = unlock)
1314
- `PATCH /billing-accounts/:billingAccountId/consume-amount` (deletes locks for challenge, then upserts consumed)
@@ -40,6 +41,18 @@ pnpm run dev
4041
pnpm run build && pnpm start
4142
```
4243

44+
## Salesforce integration
45+
46+
- This service can resolve billing accounts a user has access to via Salesforce. To enable Salesforce calls, configure the following environment variables in `.env` (see `.env.example`):
47+
- `SALESFORCE_CLIENT_ID` — Connected App client ID
48+
- `SALESFORCE_SUBJECT` — integration user username (subject for JWT)
49+
- `SALESFORCE_CLIENT_KEY` — PEM private key for the connected app (escape newlines as `\\n` in single-line `.env` files)
50+
- `SALESFORCE_AUDIENCE` — login URL (default `https://login.salesforce.com`, use `https://test.salesforce.com` for sandbox)
51+
52+
- Optional overrides for field names returned by your Salesforce org are available via `SFDC_BILLING_ACCOUNT_NAME_FIELD`, `SFDC_BILLING_ACCOUNT_MARKUP_FIELD`, and `SFDC_BILLING_ACCOUNT_ACTIVE_FIELD`.
53+
54+
- Once configured, you can call `GET /v6/billing-accounts/users/:userId` to obtain billing accounts the specified user has access to (the service authenticates to Salesforce using JWT Bearer flow and queries resource objects).
55+
4356
## Import scripts
4457

4558
- Legacy import (clients, billing accounts, challenge budgets):

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,21 @@
1919
"generate:subcontractor-sql": "ts-node scripts/generate-subcontractor-sql.ts"
2020
},
2121
"dependencies": {
22+
"@nestjs/cli": "^11.0.0",
2223
"@nestjs/common": "^10.3.0",
2324
"@nestjs/config": "^3.2.0",
2425
"@nestjs/core": "^10.3.0",
25-
"@nestjs/cli": "^11.0.0",
26-
"@nestjs/swagger": "^7.4.2",
2726
"@nestjs/mapped-types": "^2.1.0",
2827
"@nestjs/platform-express": "^10.3.0",
28+
"@nestjs/swagger": "^7.4.2",
2929
"@prisma/client": "^6.18.0",
3030
"@types/express": "^5.0.3",
31+
"axios": "^1.13.4",
3132
"class-transformer": "^0.5.1",
3233
"class-validator": "^0.14.0",
3334
"dotenv": "^16.4.5",
35+
"jsonwebtoken": "^9.0.3",
36+
"lodash": "^4.17.23",
3437
"pino": "^9.0.0",
3538
"prisma": "^6.18.0",
3639
"reflect-metadata": "^0.1.13",
@@ -41,6 +44,9 @@
4144
"devDependencies": {
4245
"@eslint/eslintrc": "^3.2.0",
4346
"@eslint/js": "^9.18.0",
47+
"@types/axios": "^0.14.4",
48+
"@types/jsonwebtoken": "^9.0.10",
49+
"@types/lodash": "^4.17.23",
4450
"@types/node": "^20.14.9",
4551
"eslint": "^9.18.0",
4652
"eslint-config-prettier": "^10.0.1",

pnpm-lock.yaml

Lines changed: 52 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/billing-accounts/billing-accounts.controller.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,29 @@ export class BillingAccountsController {
8686
return this.service.create(dto);
8787
}
8888

89+
@Get("users/:userId")
90+
@UseGuards(RolesGuard, ScopesGuard)
91+
@Roles(ADMIN_ROLE, COPILOT_ROLE)
92+
@Scopes(SCOPES.READ_BA, SCOPES.ALL_BA)
93+
@ApiOperation(
94+
buildOperationDoc({
95+
summary: "List billing accounts accessible by user",
96+
description:
97+
"Retrieve billing accounts that the given user ID has access to (via Salesforce).",
98+
jwtRoles: [ADMIN_ROLE, COPILOT_ROLE],
99+
m2mScopes: [SCOPES.READ_BA, SCOPES.ALL_BA],
100+
}),
101+
)
102+
@ApiOkResponse({ description: "List of billing accounts returned" })
103+
@ApiParam({
104+
name: "userId",
105+
description: "User ID (number)",
106+
type: Number,
107+
})
108+
async listByUserIds(@Param("userId", ParseIntPipe) userId: number) {
109+
return this.service.listByUserId(userId);
110+
}
111+
89112
@Get(":billingAccountId")
90113
@UseGuards(RolesGuard, ScopesGuard)
91114
@Roles(ADMIN_ROLE, COPILOT_ROLE)

src/billing-accounts/billing-accounts.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Module } from "@nestjs/common";
22
import { BillingAccountsController } from "./billing-accounts.controller";
33
import { BillingAccountsService } from "./billing-accounts.service";
44
import { MembersLookupService } from "../common/members-lookup.service";
5+
import SalesforceService from "../common/salesforce.service";
56

67
@Module({
78
controllers: [BillingAccountsController],
8-
providers: [BillingAccountsService, MembersLookupService],
9-
exports: [BillingAccountsService, MembersLookupService],
9+
providers: [BillingAccountsService, MembersLookupService, SalesforceService],
10+
exports: [BillingAccountsService, MembersLookupService, SalesforceService],
1011
})
1112
export class BillingAccountsModule {}

src/billing-accounts/billing-accounts.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,37 @@ import { UpdateBillingAccountDto } from "./dto/update-billing-account.dto";
77
import { LockAmountDto } from "./dto/lock-amount.dto";
88
import { ConsumeAmountDto } from "./dto/consume-amount.dto";
99
import { MembersLookupService } from "../common/members-lookup.service";
10+
import SalesforceService from "../common/salesforce.service";
1011

1112
@Injectable()
1213
export class BillingAccountsService {
1314
constructor(
1415
private readonly prisma: PrismaService,
1516
private readonly membersLookup: MembersLookupService,
17+
private readonly salesforce: SalesforceService,
1618
) {}
1719

20+
/**
21+
* List billing accounts one or more user IDs have access to (via Salesforce resource object).
22+
* Accepts a single userId (number).
23+
*/
24+
async listByUserId(userId: number) {
25+
const { accessToken, instanceUrl } = await this.salesforce.authenticate();
26+
27+
// escape backslashes and single quotes and build IN clause
28+
const escaped = `'${String(userId).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
29+
const nameField =
30+
process.env.SFDC_BILLING_ACCOUNT_NAME_FIELD || "Billing_Account_name__c";
31+
const sql = `SELECT Topcoder_Billing_Account__r.Id, Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c, Topcoder_Billing_Account__r.${nameField}, 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 = ${escaped}`;
32+
33+
const res = await this.salesforce.queryUserBillingAccounts(
34+
sql,
35+
accessToken,
36+
instanceUrl,
37+
);
38+
return res;
39+
}
40+
1841
async list(q: QueryBillingAccountsDto) {
1942
const {
2043
clientId,

0 commit comments

Comments
 (0)