# Task: Correspondence Module

**Status:** Not Started
**Priority:** P1 (High - Core Business Module)
**Estimated Effort:** 7-10 days
**Dependencies:** TASK-BE-001, TASK-BE-002, TASK-BE-003, TASK-BE-004
**Owner:** Backend Team

---

## 📋 Overview

สร้าง Correspondence Module สำหรับจัดการเอกสารโต้ตอบด้วย Master-Revision Pattern พร้อม Workflow Integration

---

## 🎯 Objectives

- ✅ CRUD Operations (Correspondences + Revisions)
- ✅ Master-Revision Pattern Implementation
- ✅ Attachment Management
- ✅ Workflow Integration (Routing)
- ✅ Document Number Generation
- ✅ Search & Filter

---

## 📝 Acceptance Criteria

1. **Basic Operations:**
   - ✅ Create correspondence (auto-generate number)
   - ✅ Create revision
   - ✅ Update correspondence/revision
   - ✅ Soft delete correspondence
   - ✅ Get correspondence with latest revision
   - ✅ Get all revisions history

2. **Attachments:**
   - ✅ Upload via two-phase storage
   - ✅ Link attachments to revision
   - ✅ Download attachments
   - ✅ Delete attachments

3. **Workflow:**
   - ✅ Submit correspondence → Create workflow instance
   - ✅ Execute workflow transitions
   - ✅ Track workflow status

4. **Search & Filter:**
   - ✅ Search by title, number, project
   - ✅ Filter by status, type, date range
   - ✅ Pagination support

---

## 🛠️ Implementation Steps

### 1. Entities

```typescript
// File: backend/src/modules/correspondence/entities/correspondence.entity.ts
@Entity('correspondences')
export class Correspondence extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 50, unique: true })
  correspondence_number: string;

  @Column({ length: 500 })
  title: string;

  @Column()
  project_id: number;

  @Column()
  originator_organization_id: number;

  @Column()
  recipient_organization_id: number;

  @Column()
  correspondence_type_id: number;

  @Column({ nullable: true })
  discipline_id: number;

  @Column({ default: 'draft' })
  status: string;

  @Column()
  created_by_user_id: number;

  @DeleteDateColumn()
  deleted_at: Date;

  // Relationships
  @ManyToOne(() => Project)
  @JoinColumn({ name: 'project_id' })
  project: Project;

  @ManyToOne(() => Organization)
  @JoinColumn({ name: 'originator_organization_id' })
  originatorOrganization: Organization;

  @OneToMany(() => CorrespondenceRevision, (rev) => rev.correspondence)
  revisions: CorrespondenceRevision[];

  @ManyToOne(() => User)
  @JoinColumn({ name: 'created_by_user_id' })
  createdBy: User;
}
```

```typescript
// File: backend/src/modules/correspondence/entities/correspondence-revision.entity.ts
@Entity('correspondence_revisions')
export class CorrespondenceRevision {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  correspondence_id: number;

  @Column({ default: 1 })
  revision_number: number;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ type: 'json', nullable: true })
  details: any; // Dynamic JSON field

  @Column()
  created_by_user_id: number;

  @CreateDateColumn()
  created_at: Date;

  // Relationships
  @ManyToOne(() => Correspondence, (corr) => corr.revisions)
  @JoinColumn({ name: 'correspondence_id' })
  correspondence: Correspondence;

  @ManyToMany(() => Attachment)
  @JoinTable({
    name: 'correspondence_attachments',
    joinColumn: { name: 'correspondence_revision_id' },
    inverseJoinColumn: { name: 'attachment_id' },
  })
  attachments: Attachment[];
}
```

### 2. Service

```typescript
// File: backend/src/modules/correspondence/correspondence.service.ts
@Injectable()
export class CorrespondenceService {
  constructor(
    @InjectRepository(Correspondence)
    private corrRepo: Repository<Correspondence>,
    @InjectRepository(CorrespondenceRevision)
    private revisionRepo: Repository<CorrespondenceRevision>,
    private fileStorage: FileStorageService,
    private docNumbering: DocumentNumberingService,
    private workflowEngine: WorkflowEngineService,
    private dataSource: DataSource
  ) {}

  async create(dto: CreateCorrespondenceDto, userId: number): Promise<Correspondence> {
    return this.dataSource.transaction(async (manager) => {
      // 1. Generate document number
      const docNumber = await this.docNumbering.generateNextNumber({
        projectId: dto.project_id,
        organizationId: dto.originator_organization_id,
        typeId: dto.correspondence_type_id,
        disciplineId: dto.discipline_id,
      });

      // 2. Create correspondence master
      const correspondence = manager.create(Correspondence, {
        correspondence_number: docNumber,
        title: dto.title,
        project_id: dto.project_id,
        originator_organization_id: dto.originator_organization_id,
        recipient_organization_id: dto.recipient_organization_id,
        correspondence_type_id: dto.correspondence_type_id,
        discipline_id: dto.discipline_id,
        status: 'draft',
        created_by_user_id: userId,
      });
      await manager.save(correspondence);

      // 3. Create initial revision
      const revision = manager.create(CorrespondenceRevision, {
        correspondence_id: correspondence.id,
        revision_number: 1,
        description: dto.description,
        details: dto.details,
        created_by_user_id: userId,
      });
      await manager.save(revision);

      // 4. Commit temp files (if any)
      if (dto.temp_file_ids?.length > 0) {
        const attachments = await this.fileStorage.commitFiles(
          dto.temp_file_ids,
          correspondence.id,
          'correspondence',
          manager
        );

        // Link attachments to revision
        revision.attachments = attachments;
        await manager.save(revision);
      }

      // 5. Create workflow instance
      const workflowInstance = await this.workflowEngine.createInstance(
        'CORRESPONDENCE_ROUTING',
        'correspondence',
        correspondence.id,
        manager
      );

      return correspondence;
    });
  }

  async createRevision(
    correspondenceId: number,
    dto: CreateRevisionDto,
    userId: number
  ): Promise<CorrespondenceRevision> {
    return this.dataSource.transaction(async (manager) => {
      // Get latest revision number
      const latestRevision = await manager.findOne(CorrespondenceRevision, {
        where: { correspondence_id: correspondenceId },
        order: { revision_number: 'DESC' },
      });

      const nextRevisionNumber = (latestRevision?.revision_number || 0) + 1;

      // Create new revision
      const revision = manager.create(CorrespondenceRevision, {
        correspondence_id: correspondenceId,
        revision_number: nextRevisionNumber,
        description: dto.description,
        details: dto.details,
        created_by_user_id: userId,
      });

      await manager.save(revision);

      // Commit temp files
      if (dto.temp_file_ids?.length > 0) {
        const attachments = await this.fileStorage.commitFiles(
          dto.temp_file_ids,
          correspondenceId,
          'correspondence',
          manager
        );

        revision.attachments = attachments;
        await manager.save(revision);
      }

      return revision;
    });
  }

  async findAll(query: SearchCorrespondenceDto): Promise<PaginatedResult<Correspondence>> {
    const queryBuilder = this.corrRepo
      .createQueryBuilder('corr')
      .leftJoinAndSelect('corr.project', 'project')
      .leftJoinAndSelect('corr.originatorOrganization', 'org')
      .leftJoinAndSelect('corr.revisions', 'revision')
      .where('corr.deleted_at IS NULL');

    // Apply filters
    if (query.project_id) {
      queryBuilder.andWhere('corr.project_id = :projectId', {
        projectId: query.project_id,
      });
    }

    if (query.status) {
      queryBuilder.andWhere('corr.status = :status', { status: query.status });
    }

    if (query.search) {
      queryBuilder.andWhere('(corr.title LIKE :search OR corr.correspondence_number LIKE :search)', {
        search: `%${query.search}%`,
      });
    }

    // Pagination
    const page = query.page || 1;
    const limit = query.limit || 20;
    const skip = (page - 1) * limit;

    const [items, total] = await queryBuilder
      .orderBy('corr.created_at', 'DESC')
      .skip(skip)
      .take(limit)
      .getManyAndCount();

    return {
      items,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findOne(id: number): Promise<Correspondence> {
    const correspondence = await this.corrRepo.findOne({
      where: { id, deleted_at: IsNull() },
      relations: ['revisions', 'revisions.attachments', 'project', 'originatorOrganization'],
      order: { revisions: { revision_number: 'DESC' } },
    });

    if (!correspondence) {
      throw new NotFoundException(`Correspondence #${id} not found`);
    }

    return correspondence;
  }

  async submitForRouting(id: number, userId: number): Promise<void> {
    const correspondence = await this.findOne(id);

    if (correspondence.status !== 'draft') {
      throw new BadRequestException('Can only submit draft correspondences');
    }

    // Execute workflow transition
    await this.workflowEngine.executeTransition(correspondence.id, 'SUBMIT', userId);

    // Update status
    await this.corrRepo.update(id, { status: 'submitted' });
  }

  async softDelete(id: number, userId: number): Promise<void> {
    const correspondence = await this.findOne(id);

    if (correspondence.status !== 'draft') {
      throw new BadRequestException('Can only delete draft correspondences');
    }

    await this.corrRepo.softDelete(id);
  }
}
```

### 3. Controller

```typescript
// File: backend/src/modules/correspondence/correspondence.controller.ts
@Controller('correspondences')
@UseGuards(JwtAuthGuard, PermissionGuard)
@ApiTags('Correspondences')
export class CorrespondenceController {
  constructor(private service: CorrespondenceService) {}

  @Post()
  @RequirePermission('correspondence.create')
  @UseInterceptors(IdempotencyInterceptor)
  async create(@Body() dto: CreateCorrespondenceDto, @CurrentUser() user: User): Promise<Correspondence> {
    return this.service.create(dto, user.user_id);
  }

  @Post(':id/revisions')
  @RequirePermission('correspondence.edit')
  async createRevision(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: CreateRevisionDto,
    @CurrentUser() user: User
  ): Promise<CorrespondenceRevision> {
    return this.service.createRevision(id, dto, user.user_id);
  }

  @Get()
  @RequirePermission('correspondence.view')
  async findAll(@Query() query: SearchCorrespondenceDto) {
    return this.service.findAll(query);
  }

  @Get(':id')
  @RequirePermission('correspondence.view')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.service.findOne(id);
  }

  @Post(':id/submit')
  @RequirePermission('correspondence.submit')
  async submit(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise<void> {
    return this.service.submitForRouting(id, user.user_id);
  }

  @Delete(':id')
  @RequirePermission('correspondence.delete')
  @HttpCode(204)
  async delete(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise<void> {
    return this.service.softDelete(id, user.user_id);
  }
}
```

---

## ✅ Testing & Verification

### 1. Unit Tests

```typescript
describe('CorrespondenceService', () => {
  it('should create correspondence with document number', async () => {
    const dto = {
      title: 'Test Correspondence',
      project_id: 1,
      originator_organization_id: 3,
      recipient_organization_id: 1,
      correspondence_type_id: 1,
    };

    const result = await service.create(dto, 1);

    expect(result.correspondence_number).toMatch(/^TEAM-RFA-\d{4}-\d{4}$/);
    expect(result.revisions).toHaveLength(1);
  });
});
```

### 2. Integration Tests

```bash
# Create correspondence
curl -X POST http://localhost:3000/correspondences \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "title": "Test Correspondence",
    "project_id": 1,
    "originator_organization_id": 3,
    "recipient_organization_id": 1,
    "correspondence_type_id": 1,
    "temp_file_ids": ["temp-id-123"]
  }'
```

---

## 📚 Related Documents

- [Data Model - Correspondences](../02-architecture/data-model.md#correspondences)
- [Functional Requirements - Correspondence](../01-requirements/03.2-correspondence.md)

---

## 📦 Deliverables

- [ ] Correspondence Entity
- [ ] CorrespondenceRevision Entity
- [ ] CorrespondenceService (CRUD + Workflow)
- [ ] CorrespondenceController
- [ ] DTOs (Create, Update, Search)
- [ ] Unit Tests (85% coverage)
- [ ] Integration Tests
- [ ] API Documentation (Swagger)

---

## 🚨 Risks & Mitigation

| Risk                      | Impact   | Mitigation                     |
| ------------------------- | -------- | ------------------------------ |
| Document number collision | Critical | Double-lock mechanism          |
| File orphans              | Medium   | Two-phase storage              |
| Workflow state mismatch   | High     | Transaction-safe state updates |

---

## 📌 Notes

- Use Master-Revision pattern (separate tables)
- Auto-generate document number on create
- Workflow integration required for submit
- Soft delete only drafts
- Pagination default: 20 items per page
