Skip to content

Commit 121ec14

Browse files
committed
Changes for hiding markup / challenge fee from copilots
1 parent b6f5f9e commit 121ec14

3 files changed

Lines changed: 80 additions & 5 deletions

File tree

src/api/project/project.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export class ProjectController {
273273
* @throws UnauthorizedException When the caller is unauthenticated.
274274
* @throws ForbiddenException When the caller lacks required permissions.
275275
* @throws NotFoundException When project or billing account is missing.
276-
* @security The service strips `markup` for non-machine callers before
276+
* @security The service strips `markup` for copilot-only callers before
277277
* returning the response payload.
278278
*/
279279
@Get(':projectId/billingAccount')

src/api/project/project.service.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ describe('ProjectService', () => {
604604
).toHaveBeenCalledWith('1001', '123');
605605
});
606606

607-
it('returns project billing account and strips markup for user tokens', async () => {
607+
it('returns project billing account and strips markup for copilot-only user tokens', async () => {
608608
prismaMock.project.findFirst.mockResolvedValue({
609609
id: BigInt(1001),
610610
billingAccountId: BigInt(12),
@@ -617,6 +617,7 @@ describe('ProjectService', () => {
617617

618618
const result = await service.getProjectBillingAccount('1001', {
619619
userId: '123',
620+
roles: [UserRole.TC_COPILOT],
620621
isMachine: false,
621622
});
622623

@@ -629,6 +630,30 @@ describe('ProjectService', () => {
629630
).toHaveBeenCalledWith('12');
630631
});
631632

633+
it('returns project billing account markup for Project Manager tokens', async () => {
634+
prismaMock.project.findFirst.mockResolvedValue({
635+
id: BigInt(1001),
636+
billingAccountId: BigInt(12),
637+
});
638+
billingAccountServiceMock.getDefaultBillingAccount.mockResolvedValue({
639+
tcBillingAccountId: '12',
640+
markup: 50,
641+
active: true,
642+
});
643+
644+
const result = await service.getProjectBillingAccount('1001', {
645+
userId: '123',
646+
roles: [UserRole.PROJECT_MANAGER],
647+
isMachine: false,
648+
});
649+
650+
expect(result).toEqual({
651+
tcBillingAccountId: '12',
652+
markup: 50,
653+
active: true,
654+
});
655+
});
656+
632657
it('returns project billing account markup for m2m tokens', async () => {
633658
prismaMock.project.findFirst.mockResolvedValue({
634659
id: BigInt(1001),

src/api/project/project.service.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ import { ProjectWithRelationsDto } from './dto/project-response.dto';
4949
import { UpgradeProjectDto } from './dto/upgrade-project.dto';
5050
import { UpdateProjectDto } from './dto/update-project.dto';
5151

52+
const BILLING_MARKUP_COPILOT_ROLES = [
53+
UserRole.COPILOT,
54+
UserRole.TC_COPILOT,
55+
];
56+
57+
const BILLING_MARKUP_VISIBLE_HUMAN_ROLES = [
58+
...ADMIN_ROLES,
59+
UserRole.MANAGER,
60+
UserRole.TOPCODER_MANAGER,
61+
UserRole.TOPCODER_ACCOUNT_MANAGER,
62+
UserRole.COPILOT_MANAGER,
63+
UserRole.PROJECT_MANAGER,
64+
UserRole.TASK_MANAGER,
65+
UserRole.TOPCODER_TASK_MANAGER,
66+
UserRole.TALENT_MANAGER,
67+
UserRole.TOPCODER_TALENT_MANAGER,
68+
];
69+
5270
interface PaginatedProjectResponse {
5371
data: ProjectWithRelationsDto[];
5472
page: number;
@@ -1038,8 +1056,9 @@ export class ProjectService {
10381056
* @param user Authenticated caller context.
10391057
* @returns Default billing account details.
10401058
* @throws NotFoundException When project or billing account is missing.
1041-
* @security Removes `markup` for non-machine callers to avoid exposing
1042-
* markup details to interactive users.
1059+
* @security Removes `markup` for copilot-only callers while preserving the
1060+
* existing response shape for M2M, Project Manager, Talent Manager, and
1061+
* administrator callers.
10431062
*/
10441063
async getProjectBillingAccount(
10451064
projectId: string,
@@ -1083,7 +1102,7 @@ export class ProjectService {
10831102
tcBillingAccountId: projectBillingAccountId,
10841103
};
10851104

1086-
if (this.isMachinePrincipal(user)) {
1105+
if (!this.shouldHideBillingAccountMarkupForCopilot(user)) {
10871106
return billingAccount;
10881107
}
10891108

@@ -2175,6 +2194,37 @@ export class ProjectService {
21752194
return false;
21762195
}
21772196

2197+
/**
2198+
* Determines whether project billing-account markup must be hidden.
2199+
*
2200+
* Copilot-only human callers should not receive raw billing-account markup.
2201+
* Manager, Talent Manager, and administrator roles retain the existing
2202+
* response shape even when the same token also carries a copilot role.
2203+
*
2204+
* @param user Authenticated caller context.
2205+
* @returns `true` when billing-account markup should be removed.
2206+
*/
2207+
private shouldHideBillingAccountMarkupForCopilot(user: JwtUser): boolean {
2208+
if (this.isMachinePrincipal(user)) {
2209+
return false;
2210+
}
2211+
2212+
const roles = user.roles || [];
2213+
const hasCopilotRole = this.permissionService.hasIntersection(
2214+
roles,
2215+
BILLING_MARKUP_COPILOT_ROLES,
2216+
);
2217+
2218+
if (!hasCopilotRole) {
2219+
return false;
2220+
}
2221+
2222+
return !this.permissionService.hasIntersection(
2223+
roles,
2224+
BILLING_MARKUP_VISIBLE_HUMAN_ROLES,
2225+
);
2226+
}
2227+
21782228
/**
21792229
* Safely extracts template phase objects from JSON payload.
21802230
*

0 commit comments

Comments
 (0)