Skip to content

Commit e077c52

Browse files
authored
Merge pull request #439 from favourawaku/staging
This PR addresses four connected issues to improve the payments and limits endpoints
2 parents 61ff59e + 8b46db7 commit e077c52

14 files changed

Lines changed: 706 additions & 35 deletions

src/common/dto/pagination.dto.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { IsInt, IsOptional, Min, Max } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
import { Type } from 'class-transformer';
4+
5+
export class PaginationDto {
6+
@ApiProperty({
7+
example: 1,
8+
description: 'Page number (starting from 1)',
9+
required: false,
10+
})
11+
@IsOptional()
12+
@Type(() => Number)
13+
@IsInt({ message: 'page must be an integer' })
14+
@Min(1, { message: 'page must be at least 1' })
15+
page: number = 1;
16+
17+
@ApiProperty({
18+
example: 20,
19+
description: 'Number of items per page (max 100)',
20+
required: false,
21+
})
22+
@IsOptional()
23+
@Type(() => Number)
24+
@IsInt({ message: 'limit must be an integer' })
25+
@Min(1, { message: 'limit must be at least 1' })
26+
@Max(100, { message: 'limit must not exceed 100' })
27+
limit: number = 20;
28+
}
29+
30+
export interface PaginatedResponse<T> {
31+
data: T[];
32+
total: number;
33+
page: number;
34+
limit: number;
35+
}

src/limits/dto/create-limit.dto.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
IsBoolean,
99
MaxLength,
1010
} from 'class-validator';
11+
import { ApiProperty } from '@nestjs/swagger';
1112

1213
export enum LimitPeriod {
1314
DAILY = 'DAILY',
@@ -16,27 +17,55 @@ export enum LimitPeriod {
1617
}
1718

1819
export class CreateLimitDto {
19-
@IsUUID()
20+
@ApiProperty({
21+
example: '123e4567-e89b-12d3-a456-426614174000',
22+
description: 'User ID (UUID)',
23+
})
24+
@IsUUID(undefined, { message: 'userId must be a valid UUID' })
2025
userId: string;
2126

22-
@IsNumber({ maxDecimalPlaces: 8 })
23-
@IsPositive()
27+
@ApiProperty({
28+
example: 1000.12345678,
29+
description: 'Per-transaction limit (max 8 decimal places) - must be positive',
30+
})
31+
@IsNumber({ maxDecimalPlaces: 8 }, { message: 'perTransactionLimit must be a number with max 8 decimal places' })
32+
@IsPositive({ message: 'perTransactionLimit must be positive' })
2433
perTransactionLimit: number;
2534

26-
@IsNumber({ maxDecimalPlaces: 8 })
27-
@IsPositive()
35+
@ApiProperty({
36+
example: 10000.5,
37+
description: 'Period limit amount (max 8 decimal places) - must be positive',
38+
})
39+
@IsNumber({ maxDecimalPlaces: 8 }, { message: 'periodLimit must be a number with max 8 decimal places' })
40+
@IsPositive({ message: 'periodLimit must be positive' })
2841
periodLimit: number;
2942

30-
@IsEnum(LimitPeriod)
43+
@ApiProperty({
44+
example: 'DAILY',
45+
enum: LimitPeriod,
46+
description: 'Limit period',
47+
required: false,
48+
})
49+
@IsEnum(LimitPeriod, { message: 'period must be one of: DAILY, WEEKLY, MONTHLY' })
3150
@IsOptional()
3251
period?: LimitPeriod;
3352

34-
@IsString()
35-
@MaxLength(12)
53+
@ApiProperty({
54+
example: 'USD',
55+
description: 'Asset code (max 12 characters)',
56+
required: false,
57+
})
58+
@IsString({ message: 'assetCode must be a string' })
59+
@MaxLength(12, { message: 'assetCode must not exceed 12 characters' })
3660
@IsOptional()
3761
assetCode?: string;
3862

39-
@IsBoolean()
63+
@ApiProperty({
64+
example: true,
65+
description: 'Whether the limit is active',
66+
required: false,
67+
})
68+
@IsBoolean({ message: 'isActive must be a boolean' })
4069
@IsOptional()
4170
isActive?: boolean;
4271
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IsEnum, IsOptional, IsBoolean } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
import { LimitPeriod } from './create-limit.dto';
4+
import { Type } from 'class-transformer';
5+
6+
export class LimitsFilterDto {
7+
@ApiProperty({
8+
example: 'DAILY',
9+
enum: LimitPeriod,
10+
description: 'Filter by limit period',
11+
required: false,
12+
})
13+
@IsEnum(LimitPeriod, { message: 'period must be one of: DAILY, WEEKLY, MONTHLY' })
14+
@IsOptional()
15+
period?: LimitPeriod;
16+
17+
@ApiProperty({
18+
example: true,
19+
description: 'Filter by active status',
20+
required: false,
21+
})
22+
@IsBoolean({ message: 'isActive must be a boolean' })
23+
@IsOptional()
24+
@Type(() => Boolean)
25+
isActive?: boolean;
26+
}

src/limits/dto/set-limits.dto.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { IsNumber, IsPositive } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class SetLimitsDto {
5+
@ApiProperty({
6+
example: 5000,
7+
description: 'Daily transaction limit amount - must be positive',
8+
})
9+
@IsNumber({}, { message: 'dailyLimit must be a number' })
10+
@IsPositive({ message: 'dailyLimit must be positive' })
11+
dailyLimit: number;
12+
13+
@ApiProperty({
14+
example: 1000,
15+
description: 'Per-transaction limit amount - must be positive',
16+
})
17+
@IsNumber({}, { message: 'perTransactionLimit must be a number' })
18+
@IsPositive({ message: 'perTransactionLimit must be positive' })
19+
perTransactionLimit: number;
20+
}

src/limits/dto/update-limit.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import { PartialType } from '@nestjs/mapped-types';
22
import { CreateLimitDto } from './create-limit.dto';
33

44
export class UpdateLimitDto extends PartialType(CreateLimitDto) {}
5+

src/limits/limits.controller.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,21 @@ describe('LimitsController', () => {
5858
await controller.removeLimits(walletId);
5959
expect(limitsService.removeLimits).toHaveBeenCalledWith(walletId);
6060
});
61+
62+
describe('swagger decorators', () => {
63+
it('should have @ApiResponse decorators on all routes', () => {
64+
const routes = ['setLimits', 'getLimits', 'removeLimits'];
65+
66+
routes.forEach((route) => {
67+
const descriptor = Object.getOwnPropertyDescriptor(
68+
LimitsController.prototype,
69+
route,
70+
);
71+
expect(descriptor).toBeDefined();
72+
73+
const metadata = Reflect.getMetadata('swagger/apiResponse', descriptor.value);
74+
expect(metadata).toBeDefined();
75+
});
76+
});
77+
});
6178
});

src/limits/limits.controller.ts

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,67 @@ import {
88
HttpCode,
99
HttpStatus,
1010
} from '@nestjs/common';
11+
import {
12+
ApiTags,
13+
ApiOperation,
14+
ApiResponse,
15+
ApiBody,
16+
ApiParam,
17+
} from '@nestjs/swagger';
1118
import { LimitsService } from './limits.service';
12-
import { IsNumber, IsPositive } from 'class-validator';
13-
14-
class SetLimitsDto {
15-
@IsNumber()
16-
@IsPositive()
17-
dailyLimit: number;
18-
19-
@IsNumber()
20-
@IsPositive()
21-
perTransactionLimit: number;
22-
}
19+
import { SetLimitsDto } from './dto/set-limits.dto';
2320

21+
@ApiTags('limits')
2422
@Controller('wallets/:walletId/limits')
2523
export class LimitsController {
2624
constructor(private readonly limitsService: LimitsService) {}
2725

26+
@ApiOperation({ summary: 'Set wallet transaction and daily limits' })
27+
@ApiParam({ name: 'walletId', description: 'Wallet ID' })
28+
@ApiBody({
29+
type: SetLimitsDto,
30+
examples: {
31+
default: {
32+
value: {
33+
dailyLimit: 5000,
34+
perTransactionLimit: 1000,
35+
},
36+
},
37+
},
38+
})
39+
@ApiResponse({
40+
status: 201,
41+
description: 'Limits set successfully',
42+
example: {
43+
walletId: '123e4567-e89b-12d3-a456-426614174000',
44+
dailyLimit: 5000,
45+
perTransactionLimit: 1000,
46+
},
47+
})
48+
@ApiResponse({
49+
status: 400,
50+
description: 'Bad request - invalid input',
51+
example: {
52+
statusCode: 400,
53+
timestamp: '2024-06-24T12:34:56.789Z',
54+
path: '/wallets/123/limits',
55+
method: 'POST',
56+
message: ['dailyLimit must be positive'],
57+
error: 'Bad Request',
58+
},
59+
})
60+
@ApiResponse({
61+
status: 404,
62+
description: 'Wallet not found',
63+
example: {
64+
statusCode: 404,
65+
timestamp: '2024-06-24T12:34:56.789Z',
66+
path: '/wallets/invalid/limits',
67+
method: 'POST',
68+
message: 'Wallet not found',
69+
error: 'Not Found',
70+
},
71+
})
2872
@Post()
2973
setLimits(@Param('walletId') walletId: string, @Body() dto: SetLimitsDto) {
3074
return this.limitsService.setLimits(
@@ -34,11 +78,52 @@ export class LimitsController {
3478
);
3579
}
3680

81+
@ApiOperation({ summary: 'Get wallet limits' })
82+
@ApiParam({ name: 'walletId', description: 'Wallet ID' })
83+
@ApiResponse({
84+
status: 200,
85+
description: 'Wallet limits retrieved successfully',
86+
example: {
87+
walletId: '123e4567-e89b-12d3-a456-426614174000',
88+
dailyLimit: 5000,
89+
perTransactionLimit: 1000,
90+
},
91+
})
92+
@ApiResponse({
93+
status: 404,
94+
description: 'No limits found for wallet',
95+
example: {
96+
statusCode: 404,
97+
timestamp: '2024-06-24T12:34:56.789Z',
98+
path: '/wallets/123/limits',
99+
method: 'GET',
100+
message: 'No limits found for wallet',
101+
error: 'Not Found',
102+
},
103+
})
37104
@Get()
38105
getLimits(@Param('walletId') walletId: string) {
39106
return this.limitsService.getLimits(walletId);
40107
}
41108

109+
@ApiOperation({ summary: 'Remove wallet limits' })
110+
@ApiParam({ name: 'walletId', description: 'Wallet ID' })
111+
@ApiResponse({
112+
status: 204,
113+
description: 'Limits removed successfully',
114+
})
115+
@ApiResponse({
116+
status: 404,
117+
description: 'No limits found for wallet',
118+
example: {
119+
statusCode: 404,
120+
timestamp: '2024-06-24T12:34:56.789Z',
121+
path: '/wallets/123/limits',
122+
method: 'DELETE',
123+
message: 'No limits found for wallet 123',
124+
error: 'Not Found',
125+
},
126+
})
42127
@Delete()
43128
@HttpCode(HttpStatus.NO_CONTENT)
44129
removeLimits(@Param('walletId') walletId: string) {

src/payments/dto/create-payment.dto.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,71 @@ import {
55
IsPositive,
66
IsOptional,
77
IsInt,
8+
Min,
89
} from 'class-validator';
10+
import { ApiProperty } from '@nestjs/swagger';
911

1012
export class CreatePaymentDto {
1113
/** Sender wallet UUID — validated to exist and be ACTIVE before payment is created. */
12-
@IsString()
13-
@IsNotEmpty()
14+
@ApiProperty({
15+
example: '123e4567-e89b-12d3-a456-426614174000',
16+
description: 'Sender wallet UUID - must exist and be ACTIVE',
17+
})
18+
@IsString({ message: 'walletId must be a string' })
19+
@IsNotEmpty({ message: 'walletId is required' })
1420
walletId: string;
1521

1622
/** Receiver wallet UUID — validated to exist before payment is created. */
17-
@IsString()
18-
@IsNotEmpty()
23+
@ApiProperty({
24+
example: '123e4567-e89b-12d3-a456-426614174001',
25+
description: 'Receiver wallet UUID - must exist',
26+
})
27+
@IsString({ message: 'receiverWalletId must be a string' })
28+
@IsNotEmpty({ message: 'receiverWalletId is required' })
1929
receiverWalletId: string;
2030

21-
@IsNumber()
22-
@IsPositive()
31+
@ApiProperty({
32+
example: 100.5,
33+
description: 'Payment amount - must be positive',
34+
})
35+
@IsNumber({}, { message: 'amount must be a number' })
36+
@IsPositive({ message: 'amount must be positive' })
2337
amount: number;
2438

25-
@IsString()
26-
@IsNotEmpty()
39+
@ApiProperty({
40+
example: 'USD',
41+
description: 'Currency code',
42+
})
43+
@IsString({ message: 'currency must be a string' })
44+
@IsNotEmpty({ message: 'currency is required' })
2745
currency: string;
2846

29-
@IsString()
47+
@ApiProperty({
48+
example: 'Payment for services',
49+
description: 'Optional payment description',
50+
required: false,
51+
})
52+
@IsString({ message: 'description must be a string' })
3053
@IsOptional()
3154
description?: string;
3255

3356
/** Legacy sender ID (LegacyUser.id) — required for payment record FK. */
34-
@IsInt()
57+
@ApiProperty({
58+
example: 1,
59+
description: 'Legacy sender ID (LegacyUser.id)',
60+
})
61+
@IsInt({ message: 'fromId must be an integer' })
62+
@IsNotEmpty({ message: 'fromId is required' })
63+
@Min(1, { message: 'fromId must be greater than 0' })
3564
fromId: number;
3665

3766
/** Legacy receiver ID (LegacyUser.id) — required for payment record FK. */
38-
@IsInt()
67+
@ApiProperty({
68+
example: 2,
69+
description: 'Legacy receiver ID (LegacyUser.id)',
70+
})
71+
@IsInt({ message: 'toId must be an integer' })
72+
@IsNotEmpty({ message: 'toId is required' })
73+
@Min(1, { message: 'toId must be greater than 0' })
3974
toId: number;
4075
}

0 commit comments

Comments
 (0)