@@ -49,6 +49,24 @@ import { ProjectWithRelationsDto } from './dto/project-response.dto';
4949import { UpgradeProjectDto } from './dto/upgrade-project.dto' ;
5050import { 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+
5270interface PaginatedProjectResponse {
5371 data : ProjectWithRelationsDto [ ] ;
5472 page : number ;
@@ -648,8 +666,10 @@ export class ProjectService {
648666 throw new ForbiddenException ( 'Insufficient permissions' ) ;
649667 }
650668
669+ const shouldClearBillingAccountId =
670+ dto . clearBillingAccountId === true || dto . billingAccountId === null ;
651671 const requestedBillingAccountId =
652- dto . clearBillingAccountId === true
672+ shouldClearBillingAccountId
653673 ? null
654674 : typeof dto . billingAccountId === 'number'
655675 ? String ( dto . billingAccountId )
@@ -708,7 +728,7 @@ export class ProjectService {
708728 cancelReason :
709729 typeof dto . cancelReason === 'string' ? dto . cancelReason : undefined ,
710730 billingAccountId :
711- dto . clearBillingAccountId === true
731+ shouldClearBillingAccountId
712732 ? null
713733 : typeof dto . billingAccountId === 'number'
714734 ? BigInt ( dto . billingAccountId )
@@ -1038,8 +1058,9 @@ export class ProjectService {
10381058 * @param user Authenticated caller context.
10391059 * @returns Default billing account details.
10401060 * @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.
1061+ * @security Removes `markup` for copilot-only callers while preserving the
1062+ * existing response shape for M2M, Project Manager, Talent Manager, and
1063+ * administrator callers.
10431064 */
10441065 async getProjectBillingAccount (
10451066 projectId : string ,
@@ -1083,7 +1104,7 @@ export class ProjectService {
10831104 tcBillingAccountId : projectBillingAccountId ,
10841105 } ;
10851106
1086- if ( this . isMachinePrincipal ( user ) ) {
1107+ if ( ! this . shouldHideBillingAccountMarkupForCopilot ( user ) ) {
10871108 return billingAccount ;
10881109 }
10891110
@@ -2175,6 +2196,37 @@ export class ProjectService {
21752196 return false ;
21762197 }
21772198
2199+ /**
2200+ * Determines whether project billing-account markup must be hidden.
2201+ *
2202+ * Copilot-only human callers should not receive raw billing-account markup.
2203+ * Manager, Talent Manager, and administrator roles retain the existing
2204+ * response shape even when the same token also carries a copilot role.
2205+ *
2206+ * @param user Authenticated caller context.
2207+ * @returns `true` when billing-account markup should be removed.
2208+ */
2209+ private shouldHideBillingAccountMarkupForCopilot ( user : JwtUser ) : boolean {
2210+ if ( this . isMachinePrincipal ( user ) ) {
2211+ return false ;
2212+ }
2213+
2214+ const roles = user . roles || [ ] ;
2215+ const hasCopilotRole = this . permissionService . hasIntersection (
2216+ roles ,
2217+ BILLING_MARKUP_COPILOT_ROLES ,
2218+ ) ;
2219+
2220+ if ( ! hasCopilotRole ) {
2221+ return false ;
2222+ }
2223+
2224+ return ! this . permissionService . hasIntersection (
2225+ roles ,
2226+ BILLING_MARKUP_VISIBLE_HUMAN_ROLES ,
2227+ ) ;
2228+ }
2229+
21782230 /**
21792231 * Safely extracts template phase objects from JSON payload.
21802232 *
0 commit comments