Skip to content

Commit bc24252

Browse files
Merge pull request DistinctCodes#361 from feyishola/feat/cost-center
cost center feat implemented
2 parents c2692cc + e522391 commit bc24252

10 files changed

+402
-1
lines changed

backend/src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { APP_GUARD } from '@nestjs/core';
1111
import { JwtAuthGuard } from './auth/guards/jwt.guard';
1212
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
1313
import { AuditsModule } from './audits/audits.module';
14+
import { CostCentersModule } from './cost-centers/cost-centers.module';
1415

1516
@Module({
1617
imports: [
@@ -62,7 +63,8 @@ import { AuditsModule } from './audits/audits.module';
6263
UsersModule,
6364
EmailModule,
6465
NewsletterModule,
65-
AuditsModule,
66+
AuditsModule,
67+
CostCentersModule,
6668
],
6769
controllers: [AppController],
6870
providers: [
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { CostCentersController } from './cost-centers.controller';
3+
import { CostCentersService } from './cost-centers.service';
4+
5+
describe('CostCentersController', () => {
6+
let controller: CostCentersController;
7+
8+
beforeEach(async () => {
9+
const module: TestingModule = await Test.createTestingModule({
10+
controllers: [CostCentersController],
11+
providers: [CostCentersService],
12+
}).compile();
13+
14+
controller = module.get<CostCentersController>(CostCentersController);
15+
});
16+
17+
it('should be defined', () => {
18+
expect(controller).toBeDefined();
19+
});
20+
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Body,
6+
Patch,
7+
Param,
8+
Delete,
9+
Query,
10+
HttpCode,
11+
HttpStatus,
12+
ParseUUIDPipe,
13+
ParseBoolPipe,
14+
} from '@nestjs/common';
15+
import {
16+
ApiTags,
17+
ApiOperation,
18+
ApiResponse,
19+
ApiQuery,
20+
ApiParam,
21+
} from '@nestjs/swagger';
22+
import { CostCentersService } from './cost-centers.service';
23+
import { CreateCostCenterDto } from './dto/create-cost-center.dto';
24+
import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
25+
import { CostCenterResponseDto } from './dto/cost-center-response.dto';
26+
27+
@ApiTags('Cost Centers')
28+
@Controller('cost-centers')
29+
export class CostCentersController {
30+
constructor(private readonly costCentersService: CostCentersService) {}
31+
32+
@Post()
33+
@ApiOperation({ summary: 'Create a new cost center' })
34+
@ApiResponse({
35+
status: 201,
36+
description: 'Cost center created successfully',
37+
type: CostCenterResponseDto,
38+
})
39+
@ApiResponse({ status: 409, description: 'Cost center name already exists' })
40+
async create(@Body() createDto: CreateCostCenterDto) {
41+
return await this.costCentersService.create(createDto);
42+
}
43+
44+
@Get()
45+
@ApiOperation({ summary: 'Get all cost centers' })
46+
@ApiQuery({
47+
name: 'includeInactive',
48+
required: false,
49+
type: Boolean,
50+
description: 'Include inactive cost centers',
51+
})
52+
@ApiResponse({
53+
status: 200,
54+
description: 'List of cost centers',
55+
type: [CostCenterResponseDto],
56+
})
57+
async findAll(
58+
@Query('includeInactive', new ParseBoolPipe({ optional: true }))
59+
includeInactive?: boolean,
60+
) {
61+
return await this.costCentersService.findAll(includeInactive);
62+
}
63+
64+
@Get(':id')
65+
@ApiOperation({ summary: 'Get a cost center by ID' })
66+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
67+
@ApiResponse({
68+
status: 200,
69+
description: 'Cost center details',
70+
type: CostCenterResponseDto,
71+
})
72+
@ApiResponse({ status: 404, description: 'Cost center not found' })
73+
async findOne(@Param('id', ParseUUIDPipe) id: string) {
74+
return await this.costCentersService.findOne(id);
75+
}
76+
77+
@Get(':id/financial-report')
78+
@ApiOperation({ summary: 'Get financial report for a cost center' })
79+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
80+
@ApiResponse({
81+
status: 200,
82+
description: 'Financial report with assets and expenses',
83+
})
84+
@ApiResponse({ status: 404, description: 'Cost center not found' })
85+
async getFinancialReport(@Param('id', ParseUUIDPipe) id: string) {
86+
return await this.costCentersService.getFinancialReport(id);
87+
}
88+
89+
@Patch(':id')
90+
@ApiOperation({ summary: 'Update a cost center' })
91+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
92+
@ApiResponse({
93+
status: 200,
94+
description: 'Cost center updated successfully',
95+
type: CostCenterResponseDto,
96+
})
97+
@ApiResponse({ status: 404, description: 'Cost center not found' })
98+
@ApiResponse({ status: 409, description: 'Cost center name already exists' })
99+
async update(
100+
@Param('id', ParseUUIDPipe) id: string,
101+
@Body() updateDto: UpdateCostCenterDto,
102+
) {
103+
return await this.costCentersService.update(id, updateDto);
104+
}
105+
106+
@Patch(':id/deactivate')
107+
@ApiOperation({ summary: 'Deactivate a cost center (soft delete)' })
108+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
109+
@ApiResponse({
110+
status: 200,
111+
description: 'Cost center deactivated',
112+
type: CostCenterResponseDto,
113+
})
114+
@ApiResponse({ status: 404, description: 'Cost center not found' })
115+
async softDelete(@Param('id', ParseUUIDPipe) id: string) {
116+
return await this.costCentersService.softDelete(id);
117+
}
118+
119+
@Patch(':id/restore')
120+
@ApiOperation({ summary: 'Restore a deactivated cost center' })
121+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
122+
@ApiResponse({
123+
status: 200,
124+
description: 'Cost center restored',
125+
type: CostCenterResponseDto,
126+
})
127+
@ApiResponse({ status: 404, description: 'Cost center not found' })
128+
async restore(@Param('id', ParseUUIDPipe) id: string) {
129+
return await this.costCentersService.restore(id);
130+
}
131+
132+
@Delete(':id')
133+
@HttpCode(HttpStatus.NO_CONTENT)
134+
@ApiOperation({ summary: 'Permanently delete a cost center' })
135+
@ApiParam({ name: 'id', description: 'Cost center UUID' })
136+
@ApiResponse({ status: 204, description: 'Cost center deleted' })
137+
@ApiResponse({ status: 404, description: 'Cost center not found' })
138+
async remove(@Param('id', ParseUUIDPipe) id: string) {
139+
await this.costCentersService.remove(id);
140+
}
141+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { CostCentersService } from './cost-centers.service';
4+
import { CostCentersController } from './cost-centers.controller';
5+
import { CostCenter } from './entities/cost-center.entity';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([CostCenter])],
9+
controllers: [CostCentersController],
10+
providers: [CostCentersService],
11+
exports: [CostCentersService], // Export service for use in other modules
12+
})
13+
export class CostCentersModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { CostCentersService } from './cost-centers.service';
3+
4+
describe('CostCentersService', () => {
5+
let service: CostCentersService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [CostCentersService],
10+
}).compile();
11+
12+
service = module.get<CostCentersService>(CostCentersService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
Injectable,
3+
NotFoundException,
4+
ConflictException,
5+
} from '@nestjs/common';
6+
import { InjectRepository } from '@nestjs/typeorm';
7+
import { Repository } from 'typeorm';
8+
import { CostCenter } from './entities/cost-center.entity';
9+
import { CreateCostCenterDto } from './dto/create-cost-center.dto';
10+
import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
11+
12+
@Injectable()
13+
export class CostCentersService {
14+
constructor(
15+
@InjectRepository(CostCenter)
16+
private readonly costCenterRepository: Repository<CostCenter>,
17+
) {}
18+
19+
async create(createDto: CreateCostCenterDto): Promise<CostCenter> {
20+
// Check if cost center with same name already exists
21+
const existing = await this.costCenterRepository.findOne({
22+
where: { name: createDto.name },
23+
});
24+
25+
if (existing) {
26+
throw new ConflictException(
27+
`Cost center with name '${createDto.name}' already exists`,
28+
);
29+
}
30+
31+
const costCenter = this.costCenterRepository.create(createDto);
32+
return await this.costCenterRepository.save(costCenter);
33+
}
34+
35+
async findAll(includeInactive = false): Promise<CostCenter[]> {
36+
const query = this.costCenterRepository.createQueryBuilder('cc');
37+
38+
if (!includeInactive) {
39+
query.where('cc.isActive = :isActive', { isActive: true });
40+
}
41+
42+
return await query.orderBy('cc.name', 'ASC').getMany();
43+
}
44+
45+
async findOne(id: string): Promise<CostCenter> {
46+
const costCenter = await this.costCenterRepository.findOne({
47+
where: { id },
48+
// Uncomment when you have relationships
49+
// relations: ['assets', 'expenses'],
50+
});
51+
52+
if (!costCenter) {
53+
throw new NotFoundException(`Cost center with ID '${id}' not found`);
54+
}
55+
56+
return costCenter;
57+
}
58+
59+
async update(
60+
id: string,
61+
updateDto: UpdateCostCenterDto,
62+
): Promise<CostCenter> {
63+
const costCenter = await this.findOne(id);
64+
65+
// Check name uniqueness if name is being updated
66+
if (updateDto.name && updateDto.name !== costCenter.name) {
67+
const existing = await this.costCenterRepository.findOne({
68+
where: { name: updateDto.name },
69+
});
70+
71+
if (existing) {
72+
throw new ConflictException(
73+
`Cost center with name '${updateDto.name}' already exists`,
74+
);
75+
}
76+
}
77+
78+
Object.assign(costCenter, updateDto);
79+
return await this.costCenterRepository.save(costCenter);
80+
}
81+
82+
async remove(id: string): Promise<void> {
83+
const costCenter = await this.findOne(id);
84+
await this.costCenterRepository.remove(costCenter);
85+
}
86+
87+
async softDelete(id: string): Promise<CostCenter> {
88+
const costCenter = await this.findOne(id);
89+
costCenter.isActive = false;
90+
return await this.costCenterRepository.save(costCenter);
91+
}
92+
93+
async restore(id: string): Promise<CostCenter> {
94+
const costCenter = await this.findOne(id);
95+
costCenter.isActive = true;
96+
return await this.costCenterRepository.save(costCenter);
97+
}
98+
99+
// Financial reporting method
100+
async getFinancialReport(id: string) {
101+
const costCenter = await this.findOne(id);
102+
103+
// This is a placeholder for financial reporting
104+
// Implement actual aggregations when you have Asset and Expense entities
105+
return {
106+
costCenter: {
107+
id: costCenter.id,
108+
name: costCenter.name,
109+
description: costCenter.description,
110+
},
111+
summary: {
112+
totalAssets: 0,
113+
totalAssetValue: 0,
114+
totalExpenses: 0,
115+
totalExpenseAmount: 0,
116+
},
117+
// assets: [],
118+
// expenses: [],
119+
};
120+
}
121+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class CostCenterResponseDto {
4+
@ApiProperty()
5+
id: string;
6+
7+
@ApiProperty()
8+
name: string;
9+
10+
@ApiProperty()
11+
description: string;
12+
13+
@ApiProperty()
14+
isActive: boolean;
15+
16+
@ApiProperty()
17+
createdAt: Date;
18+
19+
@ApiProperty()
20+
updatedAt: Date;
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { IsString, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class CreateCostCenterDto {
5+
@ApiProperty({ example: 'Marketing Department' })
6+
@IsString()
7+
@IsNotEmpty()
8+
@MaxLength(255)
9+
name: string;
10+
11+
@ApiProperty({
12+
example: 'Handles all marketing-related expenses and assets',
13+
required: false,
14+
})
15+
@IsString()
16+
@IsOptional()
17+
description?: string;
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { PartialType } from '@nestjs/swagger';
2+
import { IsBoolean, IsOptional } from 'class-validator';
3+
import { ApiProperty } from '@nestjs/swagger';
4+
import { CreateCostCenterDto } from './create-cost-center.dto';
5+
6+
export class UpdateCostCenterDto extends PartialType(CreateCostCenterDto) {
7+
@ApiProperty({ required: false })
8+
@IsBoolean()
9+
@IsOptional()
10+
isActive?: boolean;
11+
}

0 commit comments

Comments
 (0)