# Luồng nghiệp vụ — Module quản lý đơn hàng

Stack: NestJS + TypeORM + MySQL. Tài liệu này mô tả luồng xử lý gắn với 13 migration trong thư mục `migrations/`.

## 1. Tổng quan các bảng

| Bảng | Vai trò |
|---|---|
| `administrative_units` | Đơn vị hành chính 2 cấp (tỉnh/thành phố → xã/phường), có versioning cho sáp nhập |
| `administrative_unit_merges` | Crosswalk tỉnh/huyện cũ → đơn vị mới sau 1/7/2025 |
| `customers` | Khách hàng |
| `addresses` | Sổ địa chỉ của khách hàng |
| `service_types` | Loại dịch vụ (Tiết kiệm/Nhanh/Hỏa tốc) |
| `warehouses` | Bưu cục / kho phân loại |
| `couriers` | Nhân viên giao nhận |
| `fee_types` | Danh mục loại phí (mở rộng bằng INSERT, không cần migration mới) |
| `orders` | Đơn hàng — bảng trung tâm |
| `order_items` | Các loại hàng hóa trong 1 đơn (1 đơn → N hàng) |
| `order_fees` | Chi tiết từng dòng phí của 1 đơn (1 đơn → N phí) |
| `order_status_history` | Timeline trạng thái (hiển thị cho khách tra cứu) |
| `delivery_attempts` | Các lần thử giao hàng |
| `cod_transactions` | Quản lý tiền thu hộ COD |

## 2. Luồng tạo đơn hàng

1. Client gửi: thông tin sender + receiver (kèm `province_id`/`ward_id` chọn từ `administrative_units WHERE status='active'`), `service_type_id`, danh sách `items` (mỗi item: tên, số lượng, cân nặng, kích thước, giá trị khai báo), `payer`, `payment_method`.
2. `OrdersService.create()` mở 1 transaction DB:
   - Insert `orders` với `current_status = 'pending'`, các cột cache (`total_weight_gram`, `total_declared_value`, `total_fee`) tạm để 0.
   - Insert từng dòng `order_items`.
   - Tính lại cache: `total_weight_gram = SUM(items.weight_gram)`, `total_declared_value = SUM(items.declared_value)`.
   - Gọi `FeeCalculationService.calculate(order, items)` → trả về danh sách `(fee_type_id, amount)`, insert vào `order_fees`, cập nhật `orders.total_fee = SUM(order_fees.amount)`.
   - Insert `order_status_history` dòng đầu tiên (`status='pending'`, `actor_type='system'`).
   - Commit transaction, trả `order_code` cho client.
3. Toàn bộ bước 2 nằm trong **1 transaction** — nếu tính phí lỗi, đơn không được tạo.

## 3. Luồng tính phí (`FeeCalculationService`)

Lặp qua các `fee_types WHERE is_active = 1`, mỗi loại áp 1 rule riêng (rule sống ở code, `calculation_method` trên `fee_types` chỉ là metadata mô tả):

- `SHIPPING` (`formula`): tra bảng giá theo `service_type_id` + cân nặng + khoảng cách giữa `sender_province_id`/`receiver_province_id`.
- `COD_FEE` (`percentage`): % trên `cod_amount`, có mức sàn/trần.
- `INSURANCE_FEE` (`percentage`): % trên `total_declared_value`, chỉ áp nếu khách chọn bảo hiểm.
- `FUEL_SURCHARGE` (`percentage`): % trên phí ship, điều chỉnh theo kỳ.
- `REMOTE_AREA_SURCHARGE` (`fixed`): áp khi `receiver_province_id`/`ward_id` thuộc danh sách vùng xa.
- `PACKAGING_FEE`, `RETURN_FEE` (`fixed`): áp theo điều kiện nghiệp vụ tương ứng.

Thêm loại phí mới (vd phụ phí Tết) = `INSERT` vào `fee_types` + thêm 1 rule trong `FeeCalculationService` — **không cần migration**.

## 4. Luồng lấy hàng & vận chuyển nội bộ

1. Courier xác nhận lấy hàng tại địa chỉ sender → `OrdersService.markPickedUp(orderId, courierId)`:
   - Update `orders.picked_up_at`, `pickup_warehouse_id`, `current_status='picked_up'`.
   - Insert `order_status_history`.
2. Khi đơn di chuyển qua các kho (`TrackingService.updateLocation(orderId, warehouseId, status)`):
   - Update `orders.current_warehouse_id`, `current_status` (`in_transit` ↔ `at_hub`, có thể lặp nhiều vòng giữa các kho trung chuyển).
   - Insert `order_status_history` mỗi lần đổi vị trí — đây là dữ liệu hiển thị cho khách khi tra mã vận đơn.
3. Khi đến bưu cục đích, gán `assigned_courier_id`, chuyển `current_status='out_for_delivery'`.

## 5. Luồng giao hàng & xử lý thất bại

`DeliveryService.attemptDelivery(orderId, courierId, result, ...)`:

- Insert `delivery_attempts` (tăng `attempt_number`).
- Nếu `result='success'`: update `orders.delivered_at`, `current_status='delivered'`, insert `order_status_history`. Nếu `cod_amount > 0` → tạo `cod_transactions(status='collected')`.
- Nếu `result='failed'`: update `current_status='failed_delivery'`, lưu `failure_reason`. Nếu `attempt_number < MAX_ATTEMPTS` → lên lịch giao lại, chuyển lại `out_for_delivery`. Nếu vượt ngưỡng → `current_status='returned'`.

## 6. Luồng thu hộ COD & đối soát

1. Giao thành công + có COD → `cod_transactions(status='collected', collected_by=courierId)`.
2. Courier nộp tiền về bưu cục cuối ca → `status='remitted'`, set `remitted_at`.
3. Kế toán đối soát theo lô → `status='reconciled'`, set `remit_reference`. Đồng thời update `orders.payment_status` nếu liên quan.

## 7. Hủy đơn

`OrdersService.cancel(orderId, reason)` — chỉ cho phép khi `current_status IN ('pending','picked_up')`. Update `current_status='cancelled'`, insert `order_status_history` với `description=reason`.

## 8. Sơ đồ trạng thái đơn hàng

Happy path (xem diagram trong chat). Sơ đồ đầy đủ (mermaid, dán vào GitHub hoặc README để render):

```mermaid
stateDiagram-v2
  [*] --> pending
  pending --> picked_up: Lay hang thanh cong
  pending --> cancelled: Khach huy don
  picked_up --> cancelled: Huy truoc khi xuat phat
  picked_up --> in_transit: Xuat phat khoi buu cuc goc
  in_transit --> at_hub: Den kho phan loai
  at_hub --> in_transit: Chuyen tiep chang van chuyen
  at_hub --> out_for_delivery: Den buu cuc dich
  out_for_delivery --> delivered: Giao thanh cong
  out_for_delivery --> failed_delivery: Giao thay bai
  failed_delivery --> out_for_delivery: Thu giao lai (attempt < max)
  failed_delivery --> returned: Vuot so lan thu toi da
  delivered --> [*]
  returned --> [*]
  cancelled --> [*]
```

Khuyến nghị: định nghĩa transition hợp lệ ở 1 service riêng (`OrderStatusStateMachine`), từ chối mọi update không nằm trong bảng transition — tránh staff/courier set status tùy ý gây sai lệch dữ liệu.

## 9. Xử lý đơn vị hành chính sau sáp nhập

- Không xóa cứng `administrative_units` cũ — chỉ đánh dấu `status='merged'` + `merged_into_id`.
- `AdministrativeUnitsService.resolveCurrent(unitId)`: nếu `status='merged'`, theo `merged_into_id` (lặp tối đa vài lần đề phòng sáp nhập nhiều vòng) để ra đơn vị `active` hiện tại. Dùng cho hiển thị tên tỉnh/xã "hiện hành" khi tra cứu đơn cũ.
- Mọi dropdown/validation chọn tỉnh/xã ở tầng API đều phải lọc `WHERE status='active'` — không hard-code danh sách 34 tỉnh trong code.
- Khi tích hợp API VNPost EMSONE: dựng 1 adapter map giữa `administrative_units.code` nội bộ và mã mà EMSONE yêu cầu (có thể họ dùng song song mã cũ/mới trong giai đoạn chuyển tiếp) — tách lớp này riêng để không phải sửa schema core khi EMSONE đổi format.

## 10. Gợi ý cấu trúc module NestJS

```
src/modules/orders/
  orders.module.ts
  orders.controller.ts
  orders.service.ts                  # create, cancel, query
  order-status-state-machine.ts      # bang transition hop le
  fee-calculation.service.ts
  tracking.service.ts                # updateLocation, markPickedUp
  delivery.service.ts                # attemptDelivery
  cod.service.ts                     # collect, remit, reconcile
  entities/
    order.entity.ts
    order-item.entity.ts
    order-fee.entity.ts
    order-status-history.entity.ts
    delivery-attempt.entity.ts
    cod-transaction.entity.ts

src/modules/administrative-units/
  administrative-units.service.ts    # resolveCurrent, list active units
```
