Skip to content

Commit a315fd5

Browse files
Merge pull request #420 from portableDD/asset-transfer
feat(transfers): add transfer management features including approval …
2 parents 56ce7f2 + 14f83e1 commit a315fd5

21 files changed

Lines changed: 1875 additions & 84 deletions

backend/package-lock.json

Lines changed: 251 additions & 75 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"dependencies": {
2323
"@aws-sdk/client-s3": "^3.975.0",
2424
"@nestjs/bull": "^11.0.4",
25+
"@nestjs/cache-manager": "^3.1.0",
2526
"@nestjs/common": "^10.0.0",
2627
"@nestjs/config": "^3.3.0",
2728
"@nestjs/core": "^10.0.0",
@@ -30,7 +31,7 @@
3031
"@nestjs/mapped-types": "*",
3132
"@nestjs/passport": "^11.0.5",
3233
"@nestjs/platform-express": "^10.0.0",
33-
"@nestjs/schedule": "^6.0.1",
34+
"@nestjs/schedule": "^6.1.0",
3435
"@nestjs/swagger": "^7.3.0",
3536
"@nestjs/throttler": "^6.5.0",
3637
"@nestjs/typeorm": "^10.0.2",
@@ -39,9 +40,12 @@
3940
"@types/speakeasy": "^2.0.10",
4041
"@types/uuid": "^10.0.0",
4142
"axios": "^1.6.0",
43+
"bcrypt": "^6.0.0",
4244
"bcryptjs": "^3.0.3",
4345
"bull": "^4.16.5",
4446
"bwip-js": "^4.7.0",
47+
"cache-manager": "^7.2.8",
48+
"cache-manager-redis-store": "^3.0.1",
4549
"class-transformer": "^0.5.1",
4650
"class-validator": "^0.14.3",
4751
"date-fns": "^4.1.0",
@@ -71,15 +75,17 @@
7175
"@nestjs/cli": "^10.0.0",
7276
"@nestjs/schematics": "^10.0.0",
7377
"@nestjs/testing": "^10.0.0",
78+
"@types/bcrypt": "^6.0.0",
7479
"@types/bcryptjs": "^3.0.0",
7580
"@types/bull": "^3.15.9",
81+
"@types/cache-manager": "^4.0.6",
7682
"@types/express": "^5.0.0",
7783
"@types/jest": "^29.5.2",
7884
"@types/json2csv": "^5.0.7",
7985
"@types/jsonwebtoken": "^9.0.10",
80-
"@types/node": "^20.3.1",
86+
"@types/node": "^20.19.30",
8187
"@types/node-cron": "^3.0.11",
82-
"@types/nodemailer": "^7.0.5",
88+
"@types/nodemailer": "^7.0.9",
8389
"@types/otplib": "^7.0.0",
8490
"@types/papaparse": "^5.3.16",
8591
"@types/passport-jwt": "^4.0.1",

backend/src/app.module.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,20 @@ import { ReportExecution } from './reports/entities/report-execution.entity';
3434
// import { DocumentVersion } from './documents/entities/document-version.entity';
3535
// import { DocumentAccessPermission } from './documents/entities/document-access-permission.entity';
3636
// import { DocumentAuditLog } from './documents/entities/document-audit-log.entity';
37+
import { TransfersModule } from './transfers/transfers.module';
3738

3839
@Module({
3940
imports: [
4041
ConfigModule.forRoot({
4142
isGlobal: true,
4243
}),
4344
ScheduleModule.forRoot(),
44-
ThrottlerModule.forRoot([{
45-
ttl: 60000,
46-
limit: 10,
47-
}]),
45+
ThrottlerModule.forRoot([
46+
{
47+
ttl: 60000,
48+
limit: 10,
49+
},
50+
]),
4851
TypeOrmModule.forRootAsync({
4952
imports: [ConfigModule],
5053
useFactory: (configService: ConfigService) => ({
@@ -81,7 +84,8 @@ import { ReportExecution } from './reports/entities/report-execution.entity';
8184
AuditLogsModule,
8285
AssetsModule,
8386
AnalyticsModule,
84-
ReportsModule, // Add the Reports Module
87+
ReportsModule,
88+
TransfersModule, // Add the Reports Module
8589
],
8690
controllers: [AppController],
8791
providers: [
@@ -92,4 +96,4 @@ import { ReportExecution } from './reports/entities/report-execution.entity';
9296
AppService,
9397
],
9498
})
95-
export class AppModule {}
99+
export class AppModule {}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// src/transfers/controllers/transfer.controller.ts
2+
import {
3+
Controller,
4+
Get,
5+
Post,
6+
Put,
7+
Delete,
8+
Body,
9+
Param,
10+
Query,
11+
UseGuards,
12+
Request,
13+
HttpCode,
14+
HttpStatus,
15+
} from '@nestjs/common';
16+
import { ApprovalRuleService } from '../services/approval-rule.service';
17+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
18+
import { ApproveTransferDto } from '../dto/approve-transfer.dto';
19+
import { CreateApprovalRuleDto } from '../dto/create-approval-rule.dto';
20+
import { CreateTransferDto } from '../dto/create-transfer.dto';
21+
import { QueryTransfersDto } from '../dto/query-transfers.dto';
22+
import { RejectTransferDto } from '../dto/reject-transfer.dto';
23+
import { UpdateApprovalRuleDto } from '../dto/update-approval-rule.dto';
24+
import { TransferService } from '../services/transfers.service';
25+
26+
@Controller('api/v1/transfers')
27+
@UseGuards(JwtAuthGuard)
28+
export class TransferController {
29+
constructor(
30+
private readonly transferService: TransferService,
31+
private readonly approvalRuleService: ApprovalRuleService,
32+
) {}
33+
34+
@Post()
35+
async create(@Body() createTransferDto: CreateTransferDto, @Request() req) {
36+
return this.transferService.create(createTransferDto, req.user.id);
37+
}
38+
39+
@Get()
40+
async findAll(@Query() query: QueryTransfersDto) {
41+
return this.transferService.findAll(query);
42+
}
43+
44+
@Get('pending-approval')
45+
async getPendingApprovals(@Request() req) {
46+
return this.transferService.getPendingApprovals(req.user.id);
47+
}
48+
49+
@Get(':id')
50+
async findOne(@Param('id') id: string) {
51+
return this.transferService.findOne(id);
52+
}
53+
54+
@Put(':id/approve')
55+
async approve(
56+
@Param('id') id: string,
57+
@Body() dto: ApproveTransferDto,
58+
@Request() req,
59+
) {
60+
return this.transferService.approve(id, req.user.id, dto);
61+
}
62+
63+
@Put(':id/reject')
64+
async reject(
65+
@Param('id') id: string,
66+
@Body() dto: RejectTransferDto,
67+
@Request() req,
68+
) {
69+
return this.transferService.reject(id, req.user.id, dto);
70+
}
71+
72+
@Delete(':id')
73+
@HttpCode(HttpStatus.NO_CONTENT)
74+
async cancel(@Param('id') id: string, @Request() req) {
75+
await this.transferService.cancel(id, req.user.id);
76+
}
77+
78+
@Post(':id/execute')
79+
async execute(@Param('id') id: string, @Request() req) {
80+
return this.transferService.executeTransfer(id, req.user.id);
81+
}
82+
83+
@Post(':id/undo')
84+
async undo(@Param('id') id: string, @Request() req) {
85+
return this.transferService.undoTransfer(id, req.user.id);
86+
}
87+
88+
// Approval Rules endpoints
89+
@Post('approval-rules')
90+
async createRule(@Body() dto: CreateApprovalRuleDto) {
91+
return this.approvalRuleService.create(dto);
92+
}
93+
94+
@Get('approval-rules')
95+
async findAllRules() {
96+
return this.approvalRuleService.findAll();
97+
}
98+
99+
@Put('approval-rules/:id')
100+
async updateRule(
101+
@Param('id') id: string,
102+
@Body() dto: UpdateApprovalRuleDto,
103+
) {
104+
return this.approvalRuleService.update(id, dto);
105+
}
106+
107+
@Delete('approval-rules/:id')
108+
@HttpCode(HttpStatus.NO_CONTENT)
109+
async deleteRule(@Param('id') id: string) {
110+
await this.approvalRuleService.remove(id);
111+
}
112+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IsOptional, IsString } from 'class-validator';
2+
3+
export class ApproveTransferDto {
4+
@IsOptional()
5+
@IsString()
6+
notes?: string;
7+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
IsString,
3+
IsNotEmpty,
4+
IsOptional,
5+
IsBoolean,
6+
IsNumber,
7+
IsObject,
8+
IsUUID,
9+
} from 'class-validator';
10+
import { ApprovalConditions } from '../entities/transfer-approval-rule.entity';
11+
12+
export class CreateApprovalRuleDto {
13+
@IsString()
14+
@IsNotEmpty()
15+
name: string;
16+
17+
@IsOptional()
18+
@IsString()
19+
description?: string;
20+
21+
@IsObject()
22+
conditions: ApprovalConditions;
23+
24+
@IsUUID()
25+
approverRoleId: string;
26+
27+
@IsOptional()
28+
@IsUUID()
29+
approverUserId?: string;
30+
31+
@IsOptional()
32+
@IsBoolean()
33+
isActive?: boolean;
34+
35+
@IsOptional()
36+
@IsNumber()
37+
priority?: number;
38+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// src/transfers/dto/create-transfer.dto.ts
2+
import {
3+
IsEnum,
4+
IsUUID,
5+
IsArray,
6+
IsString,
7+
IsOptional,
8+
MinLength,
9+
MaxLength,
10+
IsDateString,
11+
IsNotEmpty,
12+
ArrayMinSize,
13+
ValidateIf,
14+
} from 'class-validator';
15+
import { TransferType } from '../entities/transfer.entity';
16+
17+
export class CreateTransferDto {
18+
@IsEnum(TransferType)
19+
transferType: TransferType;
20+
21+
@IsArray()
22+
@ArrayMinSize(1, { message: 'At least one asset must be selected' })
23+
@IsUUID('4', { each: true })
24+
assetIds: string[];
25+
26+
@ValidateIf(
27+
(o) =>
28+
o.transferType === TransferType.USER ||
29+
o.transferType === TransferType.COMPLETE,
30+
)
31+
@IsUUID()
32+
@IsOptional()
33+
fromUserId?: string;
34+
35+
@ValidateIf(
36+
(o) =>
37+
o.transferType === TransferType.USER ||
38+
o.transferType === TransferType.COMPLETE,
39+
)
40+
@IsUUID()
41+
@IsNotEmpty()
42+
toUserId?: string;
43+
44+
@ValidateIf(
45+
(o) =>
46+
o.transferType === TransferType.DEPARTMENT ||
47+
o.transferType === TransferType.COMPLETE,
48+
)
49+
@IsUUID()
50+
@IsOptional()
51+
fromDepartmentId?: string;
52+
53+
@ValidateIf(
54+
(o) =>
55+
o.transferType === TransferType.DEPARTMENT ||
56+
o.transferType === TransferType.COMPLETE,
57+
)
58+
@IsUUID()
59+
@IsNotEmpty()
60+
toDepartmentId?: string;
61+
62+
@ValidateIf(
63+
(o) =>
64+
o.transferType === TransferType.LOCATION ||
65+
o.transferType === TransferType.COMPLETE,
66+
)
67+
@IsUUID()
68+
@IsOptional()
69+
fromLocationId?: string;
70+
71+
@ValidateIf(
72+
(o) =>
73+
o.transferType === TransferType.LOCATION ||
74+
o.transferType === TransferType.COMPLETE,
75+
)
76+
@IsUUID()
77+
@IsNotEmpty()
78+
toLocationId?: string;
79+
80+
@IsString()
81+
@MinLength(10, { message: 'Reason must be at least 10 characters' })
82+
@MaxLength(500, { message: 'Reason must not exceed 500 characters' })
83+
reason: string;
84+
85+
@IsOptional()
86+
@IsString()
87+
notes?: string;
88+
89+
@IsOptional()
90+
@IsDateString()
91+
scheduledDate?: string;
92+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { IsOptional, IsEnum, IsUUID, IsDateString } from 'class-validator';
2+
import { TransferStatus, TransferType } from '../entities/transfer.entity';
3+
4+
export class QueryTransfersDto {
5+
@IsOptional()
6+
@IsEnum(TransferStatus)
7+
status?: TransferStatus;
8+
9+
@IsOptional()
10+
@IsEnum(TransferType)
11+
transferType?: TransferType;
12+
13+
@IsOptional()
14+
@IsUUID()
15+
requestedBy?: string;
16+
17+
@IsOptional()
18+
@IsDateString()
19+
fromDate?: string;
20+
21+
@IsOptional()
22+
@IsDateString()
23+
toDate?: string;
24+
25+
@IsOptional()
26+
page?: number = 1;
27+
28+
@IsOptional()
29+
limit?: number = 20;
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { IsString, IsNotEmpty, MinLength } from 'class-validator';
2+
3+
export class RejectTransferDto {
4+
@IsString()
5+
@IsNotEmpty()
6+
@MinLength(10, { message: 'Rejection reason must be at least 10 characters' })
7+
rejectionReason: string;
8+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { PartialType } from '@nestjs/mapped-types';
2+
import { CreateApprovalRuleDto } from './create-approval-rule.dto';
3+
4+
export class UpdateApprovalRuleDto extends PartialType(CreateApprovalRuleDto) {}

0 commit comments

Comments
 (0)