Skip to content

Commit 02651ce

Browse files
authored
Merge pull request #21 from topcoder-platform/PM-4904
PM-4904: Allow project billing account clears
2 parents 121ec14 + fcce01e commit 02651ce

4 files changed

Lines changed: 73 additions & 8 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { plainToInstance } from 'class-transformer';
2+
import { validate } from 'class-validator';
3+
import { UpdateProjectDto } from './update-project.dto';
4+
5+
describe('UpdateProjectDto', () => {
6+
it.each([null, ''])(
7+
'preserves %p billingAccountId as an explicit clear request',
8+
async (billingAccountId) => {
9+
const dto = plainToInstance(UpdateProjectDto, {
10+
billingAccountId,
11+
});
12+
13+
expect(dto.billingAccountId).toBeNull();
14+
await expect(validate(dto)).resolves.toHaveLength(0);
15+
},
16+
);
17+
18+
it('parses numeric billingAccountId updates', async () => {
19+
const dto = plainToInstance(UpdateProjectDto, {
20+
billingAccountId: '80001063',
21+
});
22+
23+
expect(dto.billingAccountId).toBe(80001063);
24+
await expect(validate(dto)).resolves.toHaveLength(0);
25+
});
26+
});

src/api/project/dto/update-project.dto.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
1-
import { ApiHideProperty } from '@nestjs/swagger';
1+
import { ApiHideProperty, ApiPropertyOptional } from '@nestjs/swagger';
22
import { Transform } from 'class-transformer';
3-
import { PartialType } from '@nestjs/mapped-types';
3+
import { IsNumber, IsOptional } from 'class-validator';
4+
import { OmitType, PartialType } from '@nestjs/mapped-types';
45
import { CreateProjectDto } from './create-project.dto';
56

7+
/**
8+
* Parses optional project billing-account update input.
9+
*
10+
* @param value Raw `billingAccountId` value from request payload.
11+
* @returns Parsed integer, `null` when clearing, or `undefined` when omitted.
12+
*/
13+
function parseOptionalNullableInteger(value: unknown): number | null | undefined {
14+
if (typeof value === 'undefined') {
15+
return undefined;
16+
}
17+
18+
if (value === null || value === '') {
19+
return null;
20+
}
21+
22+
const parsed = Number(value);
23+
24+
if (Number.isNaN(parsed)) {
25+
return undefined;
26+
}
27+
28+
return Math.trunc(parsed);
29+
}
30+
631
/**
732
* Resolves whether the patch payload explicitly requests clearing
833
* `billingAccountId`.
@@ -17,9 +42,21 @@ function parseClearBillingAccountFlag(value: unknown): boolean {
1742
/**
1843
* Request DTO for `PATCH /projects/:projectId`.
1944
*
20-
* Reuses `CreateProjectDto` and makes all fields optional via `PartialType`.
45+
* Reuses `CreateProjectDto`, makes all fields optional via `PartialType`, and
46+
* allows `billingAccountId` to be explicitly cleared with `null` or `''`.
2147
*/
22-
export class UpdateProjectDto extends PartialType(CreateProjectDto) {
48+
export class UpdateProjectDto extends PartialType(
49+
OmitType(CreateProjectDto, ['billingAccountId'] as const),
50+
) {
51+
@ApiPropertyOptional({
52+
description: 'Project billing account id. Send null or empty string to clear.',
53+
nullable: true,
54+
})
55+
@IsOptional()
56+
@Transform(({ value }) => parseOptionalNullableInteger(value))
57+
@IsNumber()
58+
billingAccountId?: number | null;
59+
2360
@ApiHideProperty()
2461
@Transform(({ obj }) => parseClearBillingAccountFlag(obj?.billingAccountId))
2562
clearBillingAccountId?: boolean;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,8 +1646,8 @@ describe('ProjectService', () => {
16461646
await service.updateProject(
16471647
'1001',
16481648
{
1649-
clearBillingAccountId: true,
1650-
} as any,
1649+
billingAccountId: null,
1650+
},
16511651
{
16521652
userId: '100',
16531653
isMachine: false,

src/api/project/project.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,10 @@ export class ProjectService {
666666
throw new ForbiddenException('Insufficient permissions');
667667
}
668668

669+
const shouldClearBillingAccountId =
670+
dto.clearBillingAccountId === true || dto.billingAccountId === null;
669671
const requestedBillingAccountId =
670-
dto.clearBillingAccountId === true
672+
shouldClearBillingAccountId
671673
? null
672674
: typeof dto.billingAccountId === 'number'
673675
? String(dto.billingAccountId)
@@ -726,7 +728,7 @@ export class ProjectService {
726728
cancelReason:
727729
typeof dto.cancelReason === 'string' ? dto.cancelReason : undefined,
728730
billingAccountId:
729-
dto.clearBillingAccountId === true
731+
shouldClearBillingAccountId
730732
? null
731733
: typeof dto.billingAccountId === 'number'
732734
? BigInt(dto.billingAccountId)

0 commit comments

Comments
 (0)