Skip to content

Commit 0107303

Browse files
committed
Add env variable for project IDs to ignore for flexi-talent tab in customer portal
1 parent be5fc6d commit 0107303

4 files changed

Lines changed: 362 additions & 23 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ TOPCODER_API_URL_BASE=https://api.topcoder-dev.com
2626
PLATFORM_UI_BASE_URL=http://localhost:3001
2727
# In production use https://platform.topcoder.com or https://platform.topcoder-dev.com
2828

29+
# ========================================
30+
# FLEXI TALENT
31+
# ========================================
32+
# Comma-separated Work project IDs excluded from Flexi Talent responses.
33+
FLEXI_TALENT_IGNORED_PROJECT_IDS=38965,1001006,1000636,37220,34654,1001120
34+
2935
# ========================================
3036
# EVENT BUS
3137
# ========================================

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Set the following environment variables (see `.env.example` for defaults):
4949
| `VALID_ISSUERS` | JSON array of allowed JWT issuers. |
5050
| `TOPCODER_API_URL_BASE` | Base URL for Topcoder API services. |
5151
| `PLATFORM_UI_BASE_URL` | Platform UI base URL used to generate anonymous feedback links. |
52+
| `FLEXI_TALENT_IGNORED_PROJECT_IDS` | Comma-separated Work project IDs excluded from all Flexi Talent engagement and member responses. |
5253
| `AUTH0_URL` | Auth0 token endpoint for M2M client credentials. |
5354
| `M2M_CLIENT_ID` | Auth0 M2M client ID. |
5455
| `M2M_CLIENT_SECRET` | Auth0 M2M client secret. |
@@ -95,6 +96,7 @@ M2M access uses Auth0 client credentials. Ensure the client is configured with t
9596
- `projectName` (if available)
9697
- `project` object with `id` and optional `name`
9798
- `PUT /engagements/:id` rejects project reassignment when the engagement's current project already has a `billingAccountId`.
99+
- Flexi Talent endpoints exclude engagements and member assignment rows whose `projectId` is configured in `FLEXI_TALENT_IGNORED_PROJECT_IDS`.
98100
- Flexi Talent list endpoints return pagination at the top level of the response body instead of the legacy nested `meta` shape:
99101

100102
```json

src/engagements/engagements.service.spec.ts

Lines changed: 240 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ const normalizeSql = (query: CapturedSqlQuery): string =>
4242
const getSqlValues = (query: CapturedSqlQuery): unknown[] => [
4343
...(query.values ?? []),
4444
];
45+
const FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV = "FLEXI_TALENT_IGNORED_PROJECT_IDS";
4546

4647
describe("EngagementsService", () => {
48+
const originalFlexiTalentIgnoredProjectIds =
49+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV];
4750
let service: EngagementsService;
4851
let db: {
4952
$queryRaw: jest.Mock;
@@ -87,6 +90,25 @@ describe("EngagementsService", () => {
8790
sendAssignmentOfferResponseEmails: jest.Mock;
8891
};
8992

93+
/**
94+
* Creates the service with the current mocks and environment.
95+
*
96+
* Tests that change environment configuration call this after setting the env
97+
* value so the service constructor reads the intended Flexi ignore list.
98+
*
99+
* @returns Service instance under test.
100+
*/
101+
const createService = (): EngagementsService =>
102+
new EngagementsService(
103+
db as any,
104+
projectService as any,
105+
skillsService as any,
106+
memberService as any,
107+
eventBusService as any,
108+
assignmentOfferEmailService as any,
109+
assignmentOfferResponseEmailService as any,
110+
);
111+
90112
const createDto = {
91113
projectId: "project-1",
92114
title: "Test Engagement",
@@ -167,6 +189,7 @@ describe("EngagementsService", () => {
167189
});
168190

169191
beforeEach(() => {
192+
delete process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV];
170193
db = {
171194
$queryRaw: jest.fn(),
172195
$transaction: jest.fn(),
@@ -210,18 +233,16 @@ describe("EngagementsService", () => {
210233
assignmentOfferResponseEmailService = {
211234
sendAssignmentOfferResponseEmails: jest.fn().mockResolvedValue(undefined),
212235
};
213-
service = new EngagementsService(
214-
db as any,
215-
projectService as any,
216-
skillsService as any,
217-
memberService as any,
218-
eventBusService as any,
219-
assignmentOfferEmailService as any,
220-
assignmentOfferResponseEmailService as any,
221-
);
236+
service = createService();
222237
});
223238

224239
afterEach(() => {
240+
if (originalFlexiTalentIgnoredProjectIds === undefined) {
241+
delete process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV];
242+
} else {
243+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] =
244+
originalFlexiTalentIgnoredProjectIds;
245+
}
225246
jest.restoreAllMocks();
226247
jest.useRealTimers();
227248
});
@@ -1558,6 +1579,126 @@ describe("EngagementsService", () => {
15581579
});
15591580
});
15601581

1582+
it("excludes configured ignored projects from Flexi engagement summaries", async () => {
1583+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] =
1584+
"38965, 1001006, 38965, ";
1585+
service = createService();
1586+
db.engagement.count
1587+
.mockResolvedValueOnce(12)
1588+
.mockResolvedValueOnce(7)
1589+
.mockResolvedValueOnce(5);
1590+
1591+
const result = await service.getFlexiEngagementSummary();
1592+
1593+
expect(db.engagement.count).toHaveBeenNthCalledWith(1, {
1594+
where: {
1595+
projectId: { notIn: ["38965", "1001006"] },
1596+
},
1597+
});
1598+
expect(db.engagement.count).toHaveBeenNthCalledWith(2, {
1599+
where: {
1600+
projectId: { notIn: ["38965", "1001006"] },
1601+
AND: [
1602+
{ status: { in: [EngagementStatus.OPEN, EngagementStatus.ACTIVE] } },
1603+
],
1604+
},
1605+
});
1606+
expect(db.engagement.count).toHaveBeenNthCalledWith(3, {
1607+
where: {
1608+
projectId: { notIn: ["38965", "1001006"] },
1609+
AND: [
1610+
{
1611+
status: {
1612+
in: [EngagementStatus.CLOSED, EngagementStatus.CANCELLED],
1613+
},
1614+
},
1615+
],
1616+
},
1617+
});
1618+
expect(result).toEqual({ total: 12, active: 7, closed: 5 });
1619+
});
1620+
1621+
it("filters ignored projects from Flexi engagement name lists and project search matches", async () => {
1622+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1623+
service = createService();
1624+
projectService.searchFlexiProjectIdsByName.mockResolvedValue([
1625+
"38965",
1626+
"project-platform",
1627+
"1001006",
1628+
]);
1629+
db.engagement.findMany.mockResolvedValue([]);
1630+
db.engagement.count.mockResolvedValue(0);
1631+
1632+
await service.getFlexiEngagementList({
1633+
bucket: FlexiEngagementBucket.Total,
1634+
searchText: "platform",
1635+
sortBy: FlexiEngagementSortBy.Name,
1636+
sortOrder: "asc",
1637+
page: 1,
1638+
perPage: 20,
1639+
} as FlexiEngagementListQueryDto);
1640+
1641+
expect(db.engagement.findMany.mock.calls[0][0].where).toEqual({
1642+
projectId: { notIn: ["38965", "1001006"] },
1643+
AND: [
1644+
{
1645+
OR: [
1646+
{
1647+
title: {
1648+
contains: "platform",
1649+
mode: "insensitive",
1650+
},
1651+
},
1652+
{ projectId: { in: ["project-platform"] } },
1653+
],
1654+
},
1655+
],
1656+
});
1657+
});
1658+
1659+
it("filters ignored projects from Flexi engagement member-count SQL", async () => {
1660+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1661+
service = createService();
1662+
db.$queryRaw
1663+
.mockResolvedValueOnce([{ total: "0" }])
1664+
.mockResolvedValueOnce([]);
1665+
1666+
await service.getFlexiEngagementList({
1667+
bucket: FlexiEngagementBucket.Total,
1668+
sortBy: FlexiEngagementSortBy.MemberCount,
1669+
sortOrder: "desc",
1670+
page: 1,
1671+
perPage: 20,
1672+
} as FlexiEngagementListQueryDto);
1673+
1674+
const countQuery = db.$queryRaw.mock.calls[0][0];
1675+
const pageQuery = db.$queryRaw.mock.calls[1][0];
1676+
1677+
expect(normalizeSql(countQuery)).toContain('e."projectId" NOT IN');
1678+
expect(normalizeSql(pageQuery)).toContain('e."projectId" NOT IN');
1679+
expect(getSqlValues(countQuery)).toEqual(
1680+
expect.arrayContaining(["38965", "1001006"]),
1681+
);
1682+
expect(getSqlValues(pageQuery)).toEqual(
1683+
expect.arrayContaining(["38965", "1001006"]),
1684+
);
1685+
});
1686+
1687+
it("returns not found for ignored Flexi engagement details", async () => {
1688+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1689+
service = createService();
1690+
db.engagement.findUnique.mockResolvedValue({
1691+
...buildFlexiEngagement({ projectId: "38965" }),
1692+
assignments: [],
1693+
});
1694+
1695+
await expect(
1696+
service.getFlexiEngagementDetail("eng-ignored"),
1697+
).rejects.toThrow("Engagement not found.");
1698+
expect(projectService.getProjectNamesByIds).not.toHaveBeenCalled();
1699+
expect(skillsService.getSkillNamesByIds).not.toHaveBeenCalled();
1700+
});
1701+
15611702
it("lists Flexi engagements by name with bucket, project search, and top-level pagination", async () => {
15621703
const query = {
15631704
bucket: FlexiEngagementBucket.Active,
@@ -1787,6 +1928,96 @@ describe("EngagementsService", () => {
17871928
});
17881929
});
17891930

1931+
it("excludes ignored project assignments from Flexi member summaries", async () => {
1932+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1933+
service = createService();
1934+
db.engagementAssignment.findMany.mockResolvedValue([]);
1935+
1936+
const result = await service.getFlexiMemberSummary();
1937+
1938+
expect(db.engagementAssignment.findMany).toHaveBeenCalledWith({
1939+
where: {
1940+
status: {
1941+
in: [
1942+
AssignmentStatus.SELECTED,
1943+
AssignmentStatus.ASSIGNED,
1944+
AssignmentStatus.OFFER_REJECTED,
1945+
AssignmentStatus.COMPLETED,
1946+
AssignmentStatus.TERMINATED,
1947+
],
1948+
},
1949+
engagement: {
1950+
projectId: {
1951+
notIn: ["38965", "1001006"],
1952+
},
1953+
},
1954+
},
1955+
select: {
1956+
memberId: true,
1957+
status: true,
1958+
},
1959+
});
1960+
expect(result).toEqual({
1961+
totalUniqueMembers: 0,
1962+
assignedMembers: 0,
1963+
completedMembers: 0,
1964+
});
1965+
});
1966+
1967+
it("excludes ignored project assignments from Flexi member list SQL", async () => {
1968+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1969+
service = createService();
1970+
db.$queryRaw
1971+
.mockResolvedValueOnce([{ total: 0n }])
1972+
.mockResolvedValueOnce([]);
1973+
1974+
await service.getFlexiMemberList(new FlexiMemberListQueryDto());
1975+
1976+
const countQuery = db.$queryRaw.mock.calls[0][0];
1977+
const pageQuery = db.$queryRaw.mock.calls[1][0];
1978+
1979+
expect(normalizeSql(countQuery)).toContain('e."projectId" NOT IN');
1980+
expect(normalizeSql(pageQuery)).toContain('e."projectId" NOT IN');
1981+
expect(getSqlValues(countQuery)).toEqual(
1982+
expect.arrayContaining(["38965", "1001006"]),
1983+
);
1984+
expect(getSqlValues(pageQuery)).toEqual(
1985+
expect.arrayContaining(["38965", "1001006"]),
1986+
);
1987+
});
1988+
1989+
it("excludes ignored project assignments from Flexi member detail lookups", async () => {
1990+
process.env[FLEXI_TALENT_IGNORED_PROJECT_IDS_ENV] = "38965,1001006";
1991+
service = createService();
1992+
db.engagementAssignment.findMany.mockResolvedValue([]);
1993+
1994+
await expect(
1995+
service.getFlexiMemberDetail("member-ignored"),
1996+
).rejects.toThrow("Member assignment history not found.");
1997+
expect(db.engagementAssignment.findMany).toHaveBeenCalledWith({
1998+
where: {
1999+
status: {
2000+
in: [
2001+
AssignmentStatus.SELECTED,
2002+
AssignmentStatus.ASSIGNED,
2003+
AssignmentStatus.OFFER_REJECTED,
2004+
AssignmentStatus.COMPLETED,
2005+
AssignmentStatus.TERMINATED,
2006+
],
2007+
},
2008+
engagement: {
2009+
projectId: {
2010+
notIn: ["38965", "1001006"],
2011+
},
2012+
},
2013+
memberId: "member-ignored",
2014+
},
2015+
include: {
2016+
engagement: true,
2017+
},
2018+
});
2019+
});
2020+
17902021
it("defaults Flexi member list requests to handle ascending", async () => {
17912022
const query = new FlexiMemberListQueryDto();
17922023

0 commit comments

Comments
 (0)