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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ Dashboard figures use these shared definitions:
Human access is limited to Administrator and Talent Manager roles. Machine
tokens require the `reports:all` scope.

## Salesforce Sales report

The read-only Sales UI and machine-only WIN sales integration dynamically run a
saved Salesforce report. See [SALES.md](SALES.md) for the two endpoints, dedicated
`reports:sales` scope, server credentials, response schema, refresh behavior,
and Salesforce completeness limits.

## Security

Currently, an M2M token is required to pull any report, and each report has its own scope associated with it that must be applied to the M2M token client ID
Report endpoints enforce their documented roles and scopes. Machine clients require the scopes granted to their client ID. The Sales UI endpoint is limited to Administrator/Talent Manager users, while the WIN Sales endpoint requires an M2M token with `reports:sales`.

The report directory (list of endpoints and parameters) is available at `GET /v6/reports/directory` and uses the same authorization rules as other endpoints. The service accepts bearer tokens from the standard `Authorization` header, and also from proxies that forward the token in `X-Authorization`/`X-Forwarded-Authorization`.

Expand Down
95 changes: 95 additions & 0 deletions SALES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Sales and WIN integration (PM-6343)

Salesforce report `00O1K00000A7UGDUA3` is the source of truth. This module executes
the saved report with `includeDetails=true` and derives every column from report
metadata. It never creates, updates or deletes Salesforce records or reports.

## Authentication

- `GET /v6/reports/sales`: authenticated human Administrator or Talent Manager.
- `GET /v6/reports/win/sales`: **machine token with `reports:sales`**. Human
administrator tokens and `reports:all` alone do not grant access.
- Register `reports:sales` on the identity provider's API resource and grant it
to the WIN client before requesting a client-credentials token. Never place a
WIN machine credential or Salesforce secret in the browser.

Both routes use the existing JWT authentication middleware, then independent
role/scope checks. Responses use `Cache-Control: private, no-store`.

## Server configuration

| Environment variable | Value |
| --- | --- |
| `SALESFORCE_API_CONSUMER_KEY` | Required connected-app consumer key, injected from secret storage |
| `SALESFORCE_API_CONSUMER_SECRET` | Required connected-app consumer secret, injected from secret storage |
| `SALESFORCE_LOGIN_URL` | Default `https://topcoder.my.salesforce.com` |
| `SALESFORCE_API_VERSION` | Default `65.0`, without the `v` prefix |
| `SALESFORCE_SALES_REPORT_ID` | Default `00O1K00000A7UGDUA3` |

Enable the connected app's OAuth Client Credentials Flow and configure a **Run
As user** with API access, permission to run reports, report-folder access, and
access to the report's underlying objects/fields. The OAuth error `no client
credentials user enabled` means this Run As setup is missing. The app starts
without these settings, but Sales requests return 503 until configured.

For the current dev deployment convention, inject the variables from secure SSM
parameters under `/config/reports-api-v6/appvar/`. Do not commit real values.

## Query and response contract

Both endpoints accept the same query parameters:

| Parameter | Meaning |
| --- | --- |
| `page`, `perPage` | One-based page (default 1); page size 1–200 (default 25) |
| `search` | Case-insensitive substring across all displayed cells, up to 200 characters |
| `filterColumn`, `filterValue` | Column ID and case-insensitive displayed-value substring; supply both |
| `sortBy`, `sortOrder` | Column ID and `asc`/`desc`; numeric and ISO date values sort before pagination |
| `refresh` | `true` to refresh, subject to the five-second minimum interval; default `false` |

Response fields: `reportId`, `reportName`, `columns[{id,label,dataType}]`,
`rows[{id,cells:[{label,value,currencyCode?}]}]`, `allData`, `sourceRowCount`,
`total`, `page`, `perPage`, `totalPages`, `refreshedAt`, `refreshAfterSeconds`.
Cells follow column order. Labels are plain text, never HTML. Currency values
retain their amount and currency code. Null values are preserved. Row IDs are
snapshot-local fact-map keys, not durable Salesforce record identifiers.
Grouping-only fields, including Stage in Bookings By Stage, precede the detail
columns and support the same filtering and sorting. Lookup names sort by their
displayed labels, while dates and currency amounts use underlying typed values.
HTML formulas are projected to text; Forecast Alert uses its image's alt label
without fetching a protected Salesforce image.

Filtering and sorting operate over the complete **received snapshot**, before
pagination. `total` is the matching received-row count; `sourceRowCount` is its
unfiltered count. Out-of-range pages clamp to the final available page. Empty
reports return zero rows and `totalPages: 0`, `page: 1`.

Salesforce Analytics limits detail responses to 2,000 rows. `allData: false`
explicitly flags an incomplete upstream snapshot; the UI warns that search,
filtering and counts apply only to returned rows. It must never be treated as a
complete export by WIN. Refine the saved Salesforce report if the limit is hit;
this API does not replace report semantics with a guessed SOQL query. Joined
reports and reports without details are rejected. See Salesforce's
[Reports API limits](https://help.salesforce.com/s/articleView?id=rd_reports_dashboards_limits.htm&language=en_US&type=5)
and [report execution contract](https://developer.salesforce.com/docs/analytics/salesforce-analytics-rest-api/guide/sforce-analytics-rest-api-getreportrundata.html).

## Freshness, failures and extension

One in-memory snapshot per service instance lasts 60 seconds. Concurrent reads
share an in-flight request; manual refresh has a five-second cooldown. No report
data is persisted. A failed refresh returns an error, with a five-second retry
cooldown, and never changes the last successful timestamp. The UI refreshes
visible pages every minute and on return to a visible tab; hidden tabs do not
poll. It displays stale-data status when a refresh fails.

OAuth and report requests time out after 15 seconds per attempt. Network
failures, HTTP 429 and 5xx retry up to three attempts with bounded backoff;
401 report responses renew OAuth once. Errors and logs omit tokens and upstream
response bodies. Validation returns 400, missing configuration 503, and upstream
failures 502. Authorization returns 401/403 before Salesforce is contacted.

Future reports can reuse `SalesforceReportsClient.runReport(reportId)` and the
metadata normalization pattern. Add explicit server-side report selection and
authorization for each; do not accept arbitrary report IDs under the sales scope.

Run `nvm use`, `pnpm lint`, `pnpm build` and `pnpm test --runInBand`.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,27 @@
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.18",
"@nestjs/platform-express": "^11.1.18",
"@nestjs/schematics": "^11.0.9",
"@nestjs/swagger": "^11.2.3",
"@nestjs/testing": "^11.1.18",
"@prisma/client": "^7.0.1",
"@types/express": "^5.0.5",
"@types/jest": "^29.5.8",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"date-fns": "^4.1.0",
"html-to-text": "^10.0.1",
"i18n-iso-countries": "^3.7.1",
"json-stringify-safe": "^5.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.2",
"tc-core-library-js": "github:topcoder-platform/tc-core-library-js#master",
"@nestjs/schematics": "^11.0.9",
"@types/jest": "^29.5.8",
"@nestjs/testing": "^11.1.18"
"tc-core-library-js": "github:topcoder-platform/tc-core-library-js#master"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@types/html-to-text": "^9.0.4",
"@types/node": "^20.11.24",
"@types/pg": "^8.15.5",
"@typescript-eslint/eslint-plugin": "^7.13.0",
Expand Down
112 changes: 112 additions & 0 deletions pnpm-lock.yaml

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

Loading
Loading