690414:1113 Update README.md /.agents/skills, /.windsurf/workflows

This commit is contained in:
2026-04-14 11:13:42 +07:00
parent 02400fd88c
commit 6d45bdaeb5
194 changed files with 12708 additions and 8762 deletions
@@ -23,6 +23,7 @@ import { CorrespondenceStatus } from '../correspondence/entities/correspondence-
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { UserService } from '../user/user.service';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
@Injectable()
export class TransmittalService {
@@ -42,10 +43,13 @@ export class TransmittalService {
private typeRepo: Repository<CorrespondenceType>,
@InjectRepository(CorrespondenceStatus)
private statusRepo: Repository<CorrespondenceStatus>,
@InjectRepository(CorrespondenceRevision)
private revisionRepo: Repository<CorrespondenceRevision>,
private numberingService: DocumentNumberingService,
private dataSource: DataSource,
private uuidResolver: UuidResolverService,
private userService: UserService
private userService: UserService,
private workflowEngine: WorkflowEngineService
) {}
async create(
@@ -192,9 +196,15 @@ export class TransmittalService {
/**
* ADR-019: Find Transmittal by parent Correspondence publicId (public identifier).
* Resolves correspondence.publicId → internal correspondenceId (INT)
* v1.8.7: Exposes workflowInstanceId, workflowState, availableActions via WorkflowEngineService
*/
async findOneByUuid(publicId: string): Promise<Transmittal> {
async findOneByUuid(publicId: string): Promise<
Transmittal & {
workflowInstanceId?: string;
workflowState?: string;
availableActions?: string[];
}
> {
const correspondence = await this.dataSource.manager.findOne(
Correspondence,
{ where: { publicId }, select: ['id'] }
@@ -204,7 +214,20 @@ export class TransmittalService {
`Transmittal with publicId ${publicId} not found`
);
}
return this.findOne(correspondence.id);
const transmittal = await this.findOne(correspondence.id);
// v1.8.7: ดึง Workflow Instance สำหรับ Transmittal นี้ (nullable — Draft ไม่มี Instance)
const wfInstance = await this.workflowEngine.getInstanceByEntity(
'transmittal',
correspondence.id.toString()
);
return {
...transmittal,
workflowInstanceId: wfInstance?.id,
workflowState: wfInstance?.currentState,
availableActions: wfInstance?.availableActions ?? [],
};
}
async findOne(id: number): Promise<Transmittal> {
@@ -217,6 +240,86 @@ export class TransmittalService {
return transmittal;
}
/**
* Submit Transmittal — ตรวจสอบ EC-RFA-004 ก่อนเริ่ม Workflow (v1.8.7)
* EC-RFA-004: ทุก item ต้องไม่อยู่ใน DRAFT ก่อน Submit
*/
async submit(
uuid: string,
user: User
): Promise<{ instanceId: string; currentState: string }> {
const correspondence = await this.dataSource.manager.findOne(
Correspondence,
{ where: { publicId: uuid }, select: ['id', 'correspondenceNumber'] }
);
if (!correspondence)
throw new NotFoundException(`Transmittal publicId ${uuid}`);
const transmittal = await this.transmittalRepo.findOne({
where: { correspondenceId: correspondence.id },
relations: ['items'],
});
if (!transmittal) throw new NotFoundException('Transmittal', uuid);
// EC-RFA-004: ตรวจสอบว่า item ทุกชิ้นไม่อยู่ใน DRAFT
if (transmittal.items && transmittal.items.length > 0) {
const itemCorrIds = transmittal.items.map((i) => i.itemCorrespondenceId);
const draftRevisions = await this.revisionRepo
.createQueryBuilder('rev')
.innerJoin('rev.status', 'status')
.where('rev.correspondenceId IN (:...ids)', { ids: itemCorrIds })
.andWhere('rev.isCurrent = :isCurrent', { isCurrent: true })
.andWhere('status.statusCode = :code', { code: 'DRAFT' })
.leftJoinAndSelect('rev.correspondence', 'corr')
.getMany();
if (draftRevisions.length > 0) {
const draftDocNo =
draftRevisions[0]?.correspondence?.correspondenceNumber ?? 'Unknown';
throw new ValidationException(
`RFA ${draftDocNo} ยังอยู่ใน Draft กรุณา Submit ก่อน`
);
}
}
// เริ่ม Workflow Instance สำหรับ Transmittal
const statusDraft = await this.statusRepo.findOne({
where: { statusCode: 'DRAFT' },
});
const instance = await this.workflowEngine.createInstance(
'TRANSMITTAL_FLOW_V1',
'transmittal',
correspondence.id.toString(),
{ ownerId: user.user_id }
);
const result = await this.workflowEngine.processTransition(
instance.id,
'SUBMIT',
user.user_id,
'Transmittal Submitted'
);
// Sync สถานะกลับที่ Correspondence Revision
if (statusDraft) {
const revision = await this.revisionRepo.findOne({
where: { correspondenceId: correspondence.id, isCurrent: true },
});
if (revision) {
const submittedStatus = await this.statusRepo.findOne({
where: { statusCode: 'SUBMITTED' },
});
if (submittedStatus) {
revision.statusId = submittedStatus.id;
await this.revisionRepo.save(revision);
}
}
}
this.logger.log(`Transmittal ${uuid} submitted — instance ${instance.id}`);
return { instanceId: instance.id, currentState: result.nextState };
}
async findAll(query: SearchTransmittalDto) {
const { page = 1, limit = 20, projectId, search } = query;
const skip = ((page ?? 1) - 1) * (limit ?? 20);
@@ -239,6 +342,13 @@ export class TransmittalService {
});
}
// B3: purpose filter (EC-RFA-004 aligned)
if (query.purpose) {
queryBuilder.andWhere('transmittal.purpose = :purpose', {
purpose: query.purpose,
});
}
if (search) {
queryBuilder.andWhere(
'(correspondence.correspondenceNumber LIKE :search OR revision.title LIKE :search)',