# Giải pháp Kiến trúc Plug & Play: Liên kết Hóa đơn và Dự án

Trong thiết kế Modular Monolith (Clean Architecture) của DSCons, việc liên kết 2 module độc lập như `invoices` (Hoá đơn) và `projects` (Dự án/WBS) đòi hỏi không được tạo ra **Sự phụ thuộc vòng (Circular Dependency)** và phải đảm bảo tính **Plug and Play** (module này có thể hoạt động độc lập ngay cả khi tháo module kia ra).

Dưới đây là phương án xử lý liên kết "Đối soát Chi phí & Gán Dự án Công trường" mà em áp dụng cho hệ thống:

## 1. Cơ chế Event-Driven (Pub/Sub) qua Module Registry

Thay vì gọi trực tiếp (Direct Call) từ module hoá đơn sang module dự án, chúng ta sử dụng **Event Bus (Message Broker nội bộ)** được tích hợp sẵn trong lõi `ModuleRegistry`.

### Luồng xử lý:
1. **[Module Hoá Đơn] Phát tín hiệu (Publish)**:
   Khi người dùng trên giao diện ấn nút **"Cập Nhật Khớp Nối"**, module Invoices sẽ lưu thông tin `matched_project_id` vào bảng `erp_invoices`.
   Sau đó, nó phát ra một sự kiện toàn cục:
   ```python
   # app/modules/invoices/application/invoice_processing_service.py
   from app.core.module_framework.registry import registry
   
   registry.event_bus.publish(
       event_name="invoice.matched_to_wbs",
       payload={
           "invoice_id": invoice.id,
           "project_id": matched_project_id,
           "wbs_id": matched_wbs_id,
           "items": items_data,
           "total_amount": invoice.total_amount_vnd
       }
   )
   ```

2. **[Module Dự Án] Lắng nghe & Xử lý (Subscribe)**:
   Nếu module `projects` đang được cắm (Plugged in) vào hệ thống, trong quá trình khởi tạo (file `__init__.py`), nó sẽ đăng ký lắng nghe sự kiện này:
   ```python
   # app/modules/projects/application/cost_sync_listener.py
   from app.core.module_framework.registry import registry

   def handle_invoice_matched(payload: dict):
       project_id = payload["project_id"]
       amount = payload["total_amount"]
       # Logic cập nhật chi phí thực tế (Actual Cost - ACWP) vào bảng erp_project_wbs
       # ...

   # Đăng ký listener lúc khởi động module
   registry.event_bus.subscribe("invoice.matched_to_wbs", handle_invoice_matched)
   ```

## 2. Thiết kế Anti-Corruption Layer (ACL) (Lớp chống tham nhũng dữ liệu)

Để đảm bảo database schema của 2 bên không bị ảnh hưởng lẫn nhau:
- **Bảng `erp_invoices`** chỉ lưu một liên kết lỏng lẻo `matched_project_id` (UUID). Nếu truy vấn cần thông tin tên dự án bên giao diện Hóa Đơn, API sẽ gọi qua một `Facade Interface` của hệ thống thay vì `JOIN` trực tiếp database cross-schema (nếu tương lai tách database microservices).
- **Hệ thống bất đồng bộ (Async)**: Việc lắng nghe và cập nhật chi phí WBS của dự án được đẩy vào hàng đợi background tasks (sử dụng Celery hoặc FastAPI BackgroundTasks). Điều này đảm bảo khi gán dự án, UI của Hóa đơn phản hồi tức thì, không bị nghẽn do tính toán chi phí phức tạp bên Dự án.

## 3. Đảm bảo Toàn vẹn Dữ liệu (Reliability & Consistency)

Cơ chế Pub/Sub nếu không thiết kế cẩn thận rất dễ gây mất dữ liệu hoặc sai lệch (bất đồng bộ). Để giải quyết triệt để vấn đề này, DSCons áp dụng 3 chốt chặn kỹ thuật:

### 3.1. Transactional Outbox Pattern (Ngăn ngừa mất Event)
Thay vì bắn Event ngay lập tức, khi lưu Hóa đơn, hệ thống sẽ lưu luôn một bản ghi vào bảng `outbox_events` trong **cùng một Transaction DB**.
- Nếu lưu Hóa đơn thất bại -> Event không được tạo (Rollback).
- Nếu lưu Hóa đơn thành công -> Event chắc chắn nằm trong `outbox_events` (Commit).
Sau đó, một Background Worker (Cronjob) sẽ đọc bảng `outbox` này để gửi Event đi. Nếu Worker chết giữa chừng, khi sống lại nó sẽ đọc và gửi tiếp các Event chưa gửi. Hoàn toàn không có khái niệm "lưu thành công nhưng rớt mạng nên mất event".

### 3.2. Idempotency (Tính Lũy Đẳng - Ngăn ngừa tính đúp chi phí)
Do mạng có thể chập chờn, một Event có thể bị gửi 2 lần (At-least-once delivery). Bảng `erp_project_wbs_costs` (bên module Dự án) thiết kế theo nguyên tắc Lũy đẳng (Idempotent).
Khi nhận Event, nó kiểm tra `invoice_id`:
- Nếu `invoice_id` này đã được ghi nhận chi phí vào dự án -> **Bỏ qua** (không tính tiền lần 2).
- Nếu chưa có -> **Thêm mới chi phí**.

### 3.3. Dead Letter Queue (DLQ) & Báo Động (Alerting)
Nếu module Dự án xử lý Event bị lỗi (VD: Không tìm thấy `wbs_id` do bị ai đó xóa mất), Event sẽ không bị vứt bỏ mà được đẩy vào **Hàng đợi lỗi (Dead Letter Queue)**.
Hệ thống sẽ lưu lại log lỗi và báo động (Telegram/War Room) để Quản trị viên vào xử lý thủ công (Retry) sau khi sửa xong dữ liệu gốc.

## 4. Lợi ích của Kiến trúc này
- **Lỏng lẻo (Decoupled)**: Nếu ta gỡ module `projects` ra, nút "Khớp nối" bên module `invoices` vẫn chạy bình thường. Event `invoice.matched_to_wbs` được phát đi nhưng không có ai nhận (Dead letter), hệ thống không bị crash.
- **Dễ mở rộng (Extensible)**: Sau này module `accounting` (Kế toán) hoặc `inventory` (Kho) có thể "nghe ké" cùng event đó để tự động tạo bút toán kép (Double-entry ledger) hoặc phiếu nhập kho mà không cần sửa code bên `invoices`.
