Skip to content
Merged
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ workflows:
branches:
only:
- develop
- PM-4949
- PM-4931

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
22 changes: 0 additions & 22 deletions .github/workflows/code_reviewer.yml

This file was deleted.

11 changes: 11 additions & 0 deletions sql/reports/sfdc/billing-account-detail.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
SELECT
ba.name,
ba.description,
ba."subcontractingEndCustomer" AS "subcontractingEndCustomer",
ba.status::text AS status,
ba."startDate" AS "startDate",
ba."endDate" AS "endDate",
ba.budget,
ba.markup
FROM "billing-accounts"."BillingAccount" ba
WHERE ba.id::text = TRIM($1)
10 changes: 10 additions & 0 deletions src/app-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const challengeReportAccessRoles = [
UserRoles.TalentManager,
] as const;

/** Human role mapping for SFDC report scopes (admins still bypass via `hasAdminRole`). */
const sfdcReportsTalentManagerRoles = [UserRoles.TalentManager] as const;

export const ScopeRoleAccess: Record<string, readonly string[]> = {
[Scopes.Challenge.History]: challengeReportAccessRoles,
[Scopes.Challenge.Registrants]: challengeReportAccessRoles,
Expand All @@ -63,6 +66,13 @@ export const ScopeRoleAccess: Record<string, readonly string[]> = {
[Scopes.Challenge.Submitters]: challengeReportAccessRoles,
[Scopes.Challenge.ValidSubmitters]: challengeReportAccessRoles,
[Scopes.Challenge.Winners]: challengeReportAccessRoles,
[Scopes.SFDC.PaymentsReport]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.ChallengesReport]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.BA]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.TaasJobs]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.TaasResourceBookings]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.TaasMemberVerification]: sfdcReportsTalentManagerRoles,
[Scopes.SFDC.WesternUnionPayments]: sfdcReportsTalentManagerRoles,
[Scopes.Member.EngagementData]: [UserRoles.TalentManager],
[Scopes.Member.RecentMemberData]: [UserRoles.TalentManager],
[Scopes.Member.MemberSearch]: [UserRoles.TalentManager],
Expand Down
42 changes: 42 additions & 0 deletions src/auth/permissions.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,46 @@ describe("permissions.util", () => {
),
).toBe(false);
});

it("allows talent manager role for SFDC payments report scope", () => {
expect(
hasAccessToScopes(
{
roles: ["Topcoder Talent Manager"],
},
[Scopes.SFDC.PaymentsReport],
),
).toBe(true);
});

it("denies product manager role for SFDC payments report scope", () => {
expect(
hasAccessToScopes(
{
roles: ["Topcoder Product Manager"],
},
[Scopes.SFDC.PaymentsReport],
),
).toBe(false);
});

it("allows administrator role for SFDC payments report scope", () => {
expect(
hasAccessToScopes(
{
roles: ["Administrator"],
},
[Scopes.SFDC.PaymentsReport],
),
).toBe(true);
});

it("allows talent manager role for other SFDC report scopes", () => {
expect(
hasAccessToScopes(
{ roles: ["Topcoder Talent Manager"] },
[Scopes.SFDC.BA, Scopes.SFDC.ChallengesReport],
),
).toBe(true);
});
});
2 changes: 1 addition & 1 deletion src/reports/member/dto/member-search-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class MatchedSkillDto {

@ApiProperty({
description:
"True when the member has at least one challenge-win event for this skill.",
"True for platform-backed skill activity (wins and/or skill events). Matched skills only include these, not self-attested-only skills.",
})
isVerified!: boolean;

Expand Down
5 changes: 4 additions & 1 deletion src/reports/member/member-search.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ describe("MemberSearchService", () => {
expect(dataSql).not.toContain(
'EXISTS (SELECT 1 FROM recently_active ra WHERE ra.user_id = m."userId")',
);
expect(dataSql).not.toContain("COALESCE(m.verified, false) = true");
});

it("adds profileComplete CTE/join only when enabled and keeps count params free of pagination", async () => {
Expand Down Expand Up @@ -226,7 +225,11 @@ describe("MemberSearchService", () => {
expect(validationParams).toEqual([[skillA, skillB]]);

expect(dataSql).toContain("requested_skills AS");
expect(dataSql).toContain("FILTER (WHERE usd.wins > 0 OR usd.submitted > 0)");
expect(dataSql).toContain("INNER JOIN user_match_data umd");
expect(dataSql).toContain("THEN COUNT(DISTINCT CASE");
expect(dataSql).toContain("ELSE COUNT(DISTINCT CASE");
expect(dataSql).toContain("(usd.wins >= rs.min_wins OR usd.submitted > 0)");
expect(dataParams).toContainEqual([skillA, skillB]);
expect(dataParams).toContainEqual([5, 0]);
expect(dataParams).toContain("AND");
Expand Down
46 changes: 28 additions & 18 deletions src/reports/member/member-search.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,31 +141,41 @@ qualifying_users AS (
GROUP BY usd.user_id
HAVING
CASE WHEN ${pSearchType} = 'AND'
THEN COUNT(DISTINCT CASE WHEN usd.wins >= rs.min_wins THEN usd.skill_id END)
THEN COUNT(DISTINCT CASE
WHEN (usd.wins >= rs.min_wins OR usd.submitted > 0)
THEN usd.skill_id END)
= ${pNumSkills}::integer
ELSE COUNT(DISTINCT CASE WHEN usd.wins >= rs.min_wins THEN usd.skill_id END) >= 1
ELSE COUNT(DISTINCT CASE
WHEN (usd.wins >= rs.min_wins OR usd.submitted > 0)
THEN usd.skill_id END) >= 1
END
),
user_match_data AS (
SELECT
usd.user_id,
SUM(
1.0
+ LEAST(usd.wins::float / 100.0, 0.5)
+ CASE WHEN usd.submitted > 0
THEN (usd.wins::float / usd.submitted::float) * 0.5
ELSE 0.0
END
COALESCE(
SUM(
1.0
+ LEAST(usd.wins::float / 100.0, 0.5)
+ CASE WHEN usd.submitted > 0
THEN (usd.wins::float / usd.submitted::float) * 0.5
ELSE 0.0
END
) FILTER (WHERE usd.wins > 0 OR usd.submitted > 0),
0.0
) AS total_skill_points,
jsonb_agg(
jsonb_build_object(
'id', usd.skill_id::text,
'name', usd.skill_name,
'isVerified', usd.wins > 0,
'wins', usd.wins,
'submitted', usd.submitted
)
ORDER BY usd.skill_name
COALESCE(
jsonb_agg(
jsonb_build_object(
'id', usd.skill_id::text,
'name', usd.skill_name,
'isVerified', (usd.wins > 0 OR usd.submitted > 0),
'wins', usd.wins,
'submitted', usd.submitted
)
ORDER BY usd.skill_name
) FILTER (WHERE usd.wins > 0 OR usd.submitted > 0),
'[]'::jsonb
) AS matched_skills
FROM user_skill_data usd
WHERE usd.user_id IN (SELECT user_id FROM qualifying_users)
Expand Down
20 changes: 20 additions & 0 deletions src/reports/sfdc/sfdc-reports.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { SfdcReportsService } from "./sfdc-reports.service";
import {
BaFeesReportQueryDto,
BaFeesReportResponse,
BillingAccountProfileQueryDto,
BillingAccountProfileResponse,
ChallengesReportQueryDto,
ChallengesReportResponse,
PaymentsReportQueryDto,
Expand Down Expand Up @@ -83,6 +85,24 @@ export class SfdcReportsController {
return report;
}

@Get("/billing-accounts")
@UseGuards(PermissionsGuard)
@Scopes(AppScopes.AllReports, AppScopes.SFDC.PaymentsReport)
@ApiBearerAuth()
@ApiOperation({
summary: "Billing account profile",
description:
"Returns billing account metadata from billing-accounts.BillingAccount when the ID exists.",
})
@ApiResponse({
status: 200,
description: "Billing account profile retrieved successfully",
type: ResponseDto<BillingAccountProfileResponse>,
})
async getBillingAccountProfile(@Query() query: BillingAccountProfileQueryDto) {
return this.reportsService.getBillingAccountProfile(query);
}

@Get("/taas/jobs")
@UseGuards(PermissionsGuard)
@Scopes(AppScopes.AllReports, AppScopes.SFDC.TaasJobs)
Expand Down
49 changes: 49 additions & 0 deletions src/reports/sfdc/sfdc-reports.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,55 @@ export class PaymentsReportResponse {
challengeStatus: string | null;
}

export class BillingAccountProfileQueryDto {
@ApiProperty({
required: true,
description:
"Billing account identifier (matches finance.payment.billing_account and billing-accounts.BillingAccount.id)",
example: "80004349",
})
@Transform(({ value }) => (typeof value === "string" ? value.trim() : value))
@IsString()
@IsNotEmpty()
billingAccountId: string;
}

export class BillingAccountDetailResponse {
@ApiProperty()
name: string;

@ApiProperty({ nullable: true, type: String })
description: string | null;

@ApiProperty({ nullable: true, type: String })
subcontractingEndCustomer: string | null;

@ApiProperty()
status: string;

@ApiProperty({ nullable: true, type: String })
startDate: string | null;

@ApiProperty({ nullable: true, type: String })
endDate: string | null;

@ApiProperty()
budget: string;

@ApiProperty()
markup: string;
}

export class BillingAccountProfileResponse {
@ApiProperty({
nullable: true,
type: BillingAccountDetailResponse,
description:
"Row from billing-accounts.BillingAccount when the ID exists; null if unknown.",
})
billingAccount: BillingAccountDetailResponse | null;
}

export class TaasJobsReportQueryDto {
@ApiProperty({
required: false,
Expand Down
17 changes: 17 additions & 0 deletions src/reports/sfdc/sfdc-reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Logger } from "src/common/logger";
import {
BaFeesReportQueryDto,
BaFeesReportResponse,
BillingAccountProfileQueryDto,
BillingAccountProfileResponse,
ChallengesFilterMode,
ChallengesReportQueryDto,
ChallengesReportResponse,
Expand Down Expand Up @@ -137,6 +139,21 @@ export class SfdcReportsService {
}));
}

async getBillingAccountProfile(
filters: BillingAccountProfileQueryDto,
): Promise<BillingAccountProfileResponse> {
const billingAccountId = filters.billingAccountId.trim();
const query = this.sql.load("reports/sfdc/billing-account-detail.sql");
const rows = await this.db.query<BillingAccountProfileResponse["billingAccount"]>(
query,
[billingAccountId],
);

return {
billingAccount: rows[0] ?? null,
};
}

async getTaasJobsReport(filters: TaasJobsReportQueryDto) {
this.logger.debug("Starting getTaasJobsReport with filters:", filters);

Expand Down
Loading