# Agency Location Data Model

## 🎯 Nguyên tắc thiết kế

### Mỗi Phường/Xã chỉ thuộc 1 Agency

**Nguyên tắc cốt lõi:**

```
Mỗi phường/xã CHỈ thuộc về 1 Agency duy nhất
→ Sử dụng Location.AgencyId để xác định ownership
→ KHÔNG cần bảng AgencyLocation (many-to-many)
```

## 📊 Cấu trúc bảng

### Location Table

```sql
CREATE TABLE Location (
    Id INT PRIMARY KEY,
    Name NVARCHAR(255),
    ParentId INT,           -- ID của location cha
    Level TINYINT,          -- 1: Tỉnh/TP, 2: Quận/Huyện, 3: Phường/Xã
    Type INT,               -- 1: Urban, 2: SubUrban, 3: NotServed
    AgencyId INT,           -- ⭐ ID của Agency sở hữu (chỉ áp dụng cho Level 3)
    DisplayOrder INT,
    ...
    FOREIGN KEY (AgencyId) REFERENCES Agency(Id)
)
```

**Hierarchy:**

```
Level 1 (Tỉnh/Thành phố) - AgencyId không sử dụng
  ├── Level 2 (Quận/Huyện) - AgencyId không sử dụng
  │    └── Level 3 (Phường/Xã) - ⭐ AgencyId xác định ownership
```

**Business Rules:**

- ✅ **Level 3 (Phường/Xã)**: `AgencyId` xác định phường thuộc agency nào
  - `AgencyId = 0`: Chưa được gán cho agency nào
  - `AgencyId > 0`: Đã được gán cho agency có ID tương ứng
- ⚠️ **Level 1, 2 (Tỉnh, Quận)**: `AgencyId` không sử dụng (luôn = 0)

## 🔄 Ví dụ cụ thể

### Dữ liệu Location

```
Id  | Name        | ParentId | Level | AgencyId | Ý nghĩa
----|-------------|----------|-------|----------|------------------
1   | TP.HCM      | 0        | 1     | 0        | Tỉnh (không dùng AgencyId)
10  | Quận 1      | 1        | 2     | 0        | Quận (không dùng AgencyId)
100 | Bến Nghé    | 10       | 3     | 1        | Phường thuộc Agency 1 ✓
101 | Bến Thành   | 10       | 3     | 1        | Phường thuộc Agency 1 ✓
11  | Quận 2      | 1        | 2     | 0        | Quận (không dùng AgencyId)
110 | An Phú      | 11       | 3     | 0        | Phường chưa gán
111 | An Khánh    | 11       | 3     | 2        | Phường thuộc Agency 2 ✓
```

### Gán coverage cho Agency

```sql
-- ❌ SAI: Không được gán tỉnh/quận
UPDATE Location SET AgencyId = 1 WHERE Id = 1;   -- TP.HCM (Level 1) - SAI!
UPDATE Location SET AgencyId = 1 WHERE Id = 10;  -- Quận 1 (Level 2) - SAI!

-- ✅ ĐÚNG: Chỉ gán phường/xã
UPDATE Location SET AgencyId = 1 WHERE Id = 100; -- Phường Bến Nghé ✓
UPDATE Location SET AgencyId = 1 WHERE Id = 101; -- Phường Bến Thành ✓

-- ✅ Bỏ gán (reset về 0)
UPDATE Location SET AgencyId = 0 WHERE Id = 100;
```

### Query để lấy coverage của Agency

```sql
-- Lấy phường/xã thuộc Agency 1
SELECT * FROM Location
WHERE AgencyId = 1 AND Level = 3
-- Result: [100: Bến Nghé, 101: Bến Thành]

-- Lấy quận/huyện (từ ParentId của phường)
SELECT DISTINCT l2.*
FROM Location l3
JOIN Location l2 ON l3.ParentId = l2.Id
WHERE l3.AgencyId = 1 AND l3.Level = 3
-- Result: [10: Quận 1]

-- Lấy tỉnh/thành phố (từ ParentId của quận)
SELECT DISTINCT l1.*
FROM Location l3
JOIN Location l2 ON l3.ParentId = l2.Id
JOIN Location l1 ON l2.ParentId = l1.Id
WHERE l3.AgencyId = 1 AND l3.Level = 3
-- Result: [1: TP.HCM]
```

### Query để lấy location available (chưa gán)

```sql
-- Lấy phường/xã chưa được gán
SELECT * FROM Location
WHERE AgencyId = 0 AND Level = 3
-- Result: [110: An Phú]

-- Lấy quận có phường chưa gán
SELECT DISTINCT l2.*
FROM Location l2
WHERE EXISTS (
    SELECT 1 FROM Location l3
    WHERE l3.ParentId = l2.Id
    AND l3.Level = 3
    AND l3.AgencyId = 0
)
-- Result: [11: Quận 2]
```

## ✅ Lợi ích của thiết kế này

### 1. Simplicity

```
❌ Trước: 2 tables (Location + AgencyLocation)
✅ Sau: 1 table (Location với AgencyId)
```

### 2. Performance

```sql
-- ❌ Trước: Cần JOIN
SELECT l.*
FROM Location l
JOIN AgencyLocation al ON l.Id = al.LocationId
WHERE al.AgencyId = 1

-- ✅ Sau: Direct query
SELECT * FROM Location
WHERE AgencyId = 1 AND Level = 3
```

### 3. Data Integrity

- ✅ **One-to-One**: Mỗi phường chỉ có thể thuộc 1 agency
- ✅ **No orphans**: Không cần maintain join table
- ✅ **Foreign Key**: Database tự enforce relationship

### 4. Business Logic

- ✅ Rõ ràng: `Location.AgencyId` = ownership
- ✅ Đơn giản: Không cần CRUD cho bảng trung gian
- ✅ Audit: UpdatedBy, UpdatedTime trực tiếp trên Location

## 🔄 API Behavior

### GetAgencyCoverage(agencyId)

```sql
-- Query phường/xã
SELECT * FROM Location WHERE AgencyId = @agencyId AND Level = 3

-- Tự động build hierarchy lên quận và tỉnh
```

### GetAvailableLocationsTree(agencyId, parentId)

```sql
-- Query locations chưa gán
SELECT * FROM Location
WHERE AgencyId = 0
AND ParentId = @parentId
```

### UpdateAgencyLocations(agencyId, locationIds[])

```sql
-- Bước 1: Reset location cũ
UPDATE Location
SET AgencyId = 0
WHERE AgencyId = @agencyId

-- Bước 2: Gán location mới (chỉ Level 3)
UPDATE Location
SET AgencyId = @agencyId
WHERE Id IN (@locationIds) AND Level = 3
```

## ⚠️ Migration từ AgencyLocation table

Nếu đang có data trong bảng `AgencyLocation` cũ:

```sql
-- Bước 1: Migrate data sang Location.AgencyId
UPDATE l
SET l.AgencyId = al.AgencyId,
    l.UpdatedTime = GETDATE()
FROM Location l
JOIN AgencyLocation al ON l.Id = al.LocationId
WHERE l.Level = 3

-- Bước 2: Verify
SELECT
    al.AgencyId,
    COUNT(*) as CountInOldTable,
    (SELECT COUNT(*) FROM Location WHERE AgencyId = al.AgencyId) as CountInNewColumn
FROM AgencyLocation al
GROUP BY al.AgencyId

-- Bước 3: Drop old table (sau khi verify)
DROP TABLE AgencyLocation
```

## 🎨 Hiển thị trên UI

### GetAgencyCoverage Response

```json
{
  "agencyId": 1,
  "locations": [
    {
      "id": 1,
      "name": "TP.HCM",
      "level": 1,
      "agencyId": 0
    },
    {
      "id": 10,
      "name": "Quận 1",
      "level": 2,
      "parentId": 1,
      "agencyId": 0
    },
    {
      "id": 100,
      "name": "Bến Nghé",
      "level": 3,
      "parentId": 10,
      "agencyId": 1
    },
    {
      "id": 101,
      "name": "Bến Thành",
      "level": 3,
      "parentId": 10,
      "agencyId": 1
    }
  ]
}
```

**Giải thích:**

- Database: Location có AgencyId = 1 là các phường (100, 101)
- API tự động query thêm Quận 1 và TP.HCM để build hierarchy
- Frontend nhận full tree để display

## 🔍 Testing Checklist

- [ ] API reject khi submit LocationId của tỉnh (Level 1)
- [ ] API reject khi submit LocationId của quận (Level 2)
- [ ] API accept khi submit LocationId của phường (Level 3)
- [ ] API reject khi phường đã thuộc agency khác
- [ ] GetAgencyCoverage trả về đầy đủ tỉnh + quận + phường
- [ ] GetAvailableLocationsTree chỉ hiện locations với AgencyId = 0
- [ ] UpdateAgencyLocations reset AgencyId = 0 cho locations cũ
- [ ] UpdateAgencyLocations set AgencyId cho locations mới
- [ ] Frontend chỉ cho phép chọn phường/xã
- [ ] Error message rõ ràng khi validation fail

## 📝 Notes

### AgencyId = 0 vs NULL

- **Sử dụng `AgencyId = 0`** thay vì NULL
- Lý do: Dễ query và không cần handle NULL
- Consistent với convention của project

### Index Recommendations

```sql
-- Index cho query by AgencyId
CREATE INDEX IX_Location_AgencyId ON Location(AgencyId)
WHERE Level = 3

-- Index cho query available locations
CREATE INDEX IX_Location_AgencyId_Level ON Location(AgencyId, Level)

-- Index cho hierarchy query
CREATE INDEX IX_Location_ParentId_Level ON Location(ParentId, Level)
```

### Validation Rules

1. ✅ Chỉ Location Level 3 mới được set AgencyId > 0
2. ✅ Location Level 1, 2 luôn có AgencyId = 0
3. ✅ Một phường chỉ thuộc 1 agency tại một thời điểm
4. ✅ Không cho phép gán phường đã thuộc agency khác
