690503:1547 Update #01
CI / CD Pipeline / build (push) Successful in 5m13s
CI / CD Pipeline / deploy (push) Successful in 4m18s

This commit is contained in:
2026-05-03 15:47:59 +07:00
parent 912b25bd06
commit 42a6d24318
10 changed files with 452 additions and 354 deletions
@@ -111,4 +111,17 @@ export class CirculationController {
) {
return this.circulationService.forceClose(uuid, dto.reason, user);
}
@Post(':uuid/close')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary:
'Close a Circulation when all Main/Action routings are completed (FR-C09)',
})
@ApiParam({ name: 'uuid', description: 'Circulation publicId' })
@RequirePermission('circulation.manage')
@Audit('circulation.close', 'circulation')
close(@Param('uuid', ParseUuidPipe) uuid: string, @CurrentUser() user: User) {
return this.circulationService.close(uuid, user);
}
}
@@ -299,6 +299,47 @@ export class CirculationService {
}
}
/**
* EC-CIRC-00X: Close Circulation (FR-C09)
* ต้องมีสิทธิ์ circulation.close (หรือ circulation.manage) เช็คใน controller
*/
async close(publicId: string, user: User) {
const circulation = await this.circulationRepo.findOne({
where: { publicId },
relations: ['routings'],
});
if (!circulation)
throw new NotFoundException(`Circulation publicId ${publicId}`);
if (
circulation.statusCode === 'COMPLETED' ||
circulation.statusCode === 'CANCELLED' ||
circulation.statusCode === 'CLOSED'
) {
throw new ValidationException(
`ใบเวียน ${circulation.circulationNo} ปิดไปแล้ว (${circulation.statusCode})`
);
}
const pendingCount = circulation.routings.filter(
(r) => r.status === 'PENDING' || r.status === 'IN_PROGRESS'
).length;
if (pendingCount > 0) {
throw new ValidationException(
'All Main/Action routings must be COMPLETED before closing'
);
}
circulation.statusCode = 'CLOSED';
circulation.closedAt = new Date();
await this.circulationRepo.save(circulation);
this.logger.log(`Circulation ${publicId} closed by user ${user.user_id}`);
return { success: true };
}
// ✅ Logic อัปเดตสถานะและปิดงาน
async updateRoutingStatus(
routingId: number,
@@ -24,6 +24,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
import { SearchModule } from '../search/search.module';
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
import { NotificationModule } from '../notification/notification.module';
import { CirculationModule } from '../circulation/circulation.module';
/**
* CorrespondenceModule
@@ -51,6 +52,7 @@ import { NotificationModule } from '../notification/notification.module';
SearchModule,
FileStorageModule,
NotificationModule,
CirculationModule,
],
controllers: [CorrespondenceController],
providers: [
@@ -20,6 +20,7 @@ import { SearchService } from '../search/search.service';
import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { NotificationService } from '../notification/notification.service';
import { CirculationService } from '../circulation/circulation.service';
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
import { User } from '../user/entities/user.entity';
@@ -158,6 +159,12 @@ describe('CorrespondenceService', () => {
provide: getRepositoryToken(CorrespondenceRevisionAttachment),
useValue: createMockRepository(),
},
{
provide: CirculationService,
useValue: {
forceClose: jest.fn().mockResolvedValue({ success: true }),
},
},
],
}).compile();
@@ -1,6 +1,6 @@
// File: src/modules/correspondence/correspondence.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import {
BusinessException,
NotFoundException,
@@ -39,6 +39,9 @@ import { SearchService } from '../search/search.service';
import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { NotificationService } from '../notification/notification.service';
import { CirculationService } from '../circulation/circulation.service';
import { Circulation } from '../circulation/entities/circulation.entity';
import { CirculationRouting } from '../circulation/entities/circulation-routing.entity';
/**
* CorrespondenceService - Document management (CRUD)
@@ -92,7 +95,9 @@ export class CorrespondenceService {
private uuidResolver: UuidResolverService,
private notificationService: NotificationService,
@InjectRepository(CorrespondenceRevisionAttachment)
private revAttachRepo: Repository<CorrespondenceRevisionAttachment>
private revAttachRepo: Repository<CorrespondenceRevisionAttachment>,
@Inject(forwardRef(() => CirculationService))
private circulationService: CirculationService
) {}
/**
@@ -984,11 +989,11 @@ export class CorrespondenceService {
}
// Check if there are any active circulations
const circulationRepo = this.dataSource.getRepository('Circulation');
const circulationRepo = this.dataSource.getRepository(Circulation);
const activeCirculations = await circulationRepo.find({
where: {
correspondenceId: correspondence.id,
status: 'OPEN',
statusCode: 'OPEN',
},
});
@@ -1033,24 +1038,46 @@ export class CorrespondenceService {
}
);
await queryRunner.commitTransaction();
// Force close all active circulations
if (activeCirculations.length > 0) {
await queryRunner.manager.update(
'Circulation',
{
correspondenceId: correspondence.id,
status: 'OPEN',
},
{
status: 'FORCE_CLOSED',
closedAt: new Date(),
closedBy: user.user_id,
closeReason: `Correspondence cancelled: ${reason}`,
}
);
}
for (const circ of activeCirculations) {
try {
await this.circulationService.forceClose(
circ.publicId,
`Correspondence cancelled: ${reason}`,
user
);
await queryRunner.commitTransaction();
// T012: Enqueue BullMQ notification for affected assignees
// CirculationService.forceClose already updates status, we just need to notify.
// Ideally we'd notify the people who were pending.
const circWithRoutings = await this.dataSource
.getRepository(CirculationRouting)
.find({
where: { circulationId: circ.id, status: 'REJECTED' },
});
for (const r of circWithRoutings) {
if (r.assignedTo) {
void this.notificationService.send({
userId: r.assignedTo,
title: 'Circulation Force Closed',
message: `ใบเวียน ${circ.circulationNo} ถูกปิดแบบบังคับ เนื่องจากเอกสารต้นทางถูกยกเลิก`,
type: 'EMAIL',
entityType: 'circulation',
entityId: circ.id,
link: `/circulations/${circ.publicId}`,
});
}
}
} catch (e) {
this.logger.error(
`Failed to force close circulation ${circ.publicId}: ${(e as Error).message}`
);
}
}
}
// Re-index cancelled status in Elasticsearch (fire-and-forget)
void this.searchService.indexDocument({
-1
View File
@@ -1,6 +1,5 @@
import _request from 'supertest';
import { AppModule } from '../src/app.module';
import { RoutingTemplate } from '../src/modules/correspondence/entities/routing-template.entity';
import { Test, TestingModule } from '@nestjs/testing';