# TSHIRTORDER-1601 — Leveranssätt: thêm option mới + ẩn Postnord fraktetikett cho 1 số loại giao hàng

## Yêu cầu gốc

> Leveranssätt add text + hide postnord on some options.

Kèm ảnh mockup (xem `files/mockup-leveranssatt-hide-postnord.png`) — chụp màn hình trang chi tiết order (React frontend), khoanh vùng 2 chỗ:
- Dropdown **Leveranssätt** đang mở, đánh dấu 3 option bằng chữ **A**, **B**, **C**: A = "Bulk", B = "Hämtas i gällstad", C = "Annan leveranssätt" (đang được chọn).
- Khối **Postnord fraktetikett** bên phải, mũi tên "Hide this when select A or B or C" trỏ vào toàn bộ khối này.

> Theo tôi hiểu: nếu order có delivery type nằm thuộc A, B hoặc C thì không cho phép submit postnord. Những option này nằm trong bảng `status_list` với `type = StatusList::TYPE_ORDER_DELIVERY_TYPE`. Cái A & C đã có sẵn `unique_key` = `delivery_type_bulk` & `delivery_type_annan_leveranssatt`. Còn cái B ("Hämtas i gällstad") chưa có, cần insert vào.

Tóm tắt 2 việc:
1. **Thêm option mới** "Hämtas i gällstad" vào danh sách Leveranssätt (bảng `status_list`, type `order_delivery_type`) — hiện chưa tồn tại.
2. **Chặn tạo Postnord fraktetikett** khi order đang có delivery type thuộc nhóm A/B/C (Bulk, Hämtas i gällstad, Annan leveranssätt).

---

## Tổng quan

Thuộc phần **API (backend)** — bảng `status_list` + `PostNordService`. Phần ẩn UI khối "Postnord fraktetikett" và dropdown Leveranssätt nằm ở **React frontend, ngoài repo này** (theo `CLAUDE.md`, frontend tiêu thụ `/api/v1/`).

> ⚠️ **Đã kiểm tra DB hiện tại** (`status_list` type `order_delivery_type`, `date_deleted IS NULL`):

| id | name | unique_key |
|----|------|------------|
| 284 | Annan leveranssätt | `delivery_type_annan_leveranssatt` |
| 285 | Bulk | `delivery_type_bulk` |
| 286 | Direkt leverans | *(rỗng)* |
| 287 | Delleverans | *(rỗng)* |

→ Xác nhận đúng như mô tả: A (Bulk) và C (Annan leveranssätt) đã có `unique_key` sẵn, dùng được ngay. **"Hämtas i gällstad" (B) chưa tồn tại trong bảng, cần tạo mới.**

> ⚠️ **Đã kiểm tra `PostNordService::orderPostnordCreateLabel()`** (`src/Service/PostNordService.php:487`) — hiện **hoàn toàn chưa có bất kỳ check nào liên quan đến delivery type**. Đây là lỗ hổng: nếu chỉ ẩn nút ở FE, ai đó vẫn có thể gọi thẳng API `POST /{id}/postnord/create-label` để tạo label cho order thuộc nhóm A/B/C. Cần chặn ở backend (không chỉ ẩn UI).

---

## Luồng hoạt động / Chi tiết theo mockup

Theo mockup (`files/mockup-leveranssatt-hide-postnord.png`):

- Dropdown **Leveranssätt** (field `deliveryTypeId` trên order) có các option, trong đó 3 option bị đánh dấu:
  - **A** = "Bulk" (`unique_key = delivery_type_bulk`, id hiện tại = 285)
  - **B** = "Hämtas i gällstad" (chưa có, cần tạo `unique_key` mới)
  - **C** = "Annan leveranssätt" (`unique_key = delivery_type_annan_leveranssatt`, id hiện tại = 284)
- Khối **"Postnord fraktetikett"** (form tạo vận đơn PostNord, gồm "Välj tjänst", "Antal kolli", nút "Skapa etikett") nằm bên phải trang order detail — khi order đang chọn Leveranssätt thuộc A, B, hoặc C thì **cả khối này phải bị ẩn/khoá**, không cho tạo fraktetikett.

---

## Questions / Đã xác nhận

| # | Câu hỏi | Trả lời | Ghi chú kỹ thuật |
|---|---------|---------|-------------------|
| 1 | Tên hiển thị chính xác + `unique_key` cho option B? | Dùng đúng đề xuất | `name = "Hämtas i gällstad"`, `unique_key = delivery_type_hamtas_i_gallstad` (theo pattern 2 key hiện có: `delivery_type_bulk`, `delivery_type_annan_leveranssatt`) |
| 2 | Khi bị chặn, API trả lỗi 400 hay chỉ FE ẩn nút? | Chặn ở backend, trả lỗi 400 | `PostNordService::orderPostnordCreateLabel()` trả `status_code = 400` khi delivery type thuộc nhóm blocked — không chỉ dựa vào FE ẩn nút |

---

## API contract / Thiết kế kỹ thuật

### 1. Thêm StatusList entry mới cho "Hämtas i gällstad"

> ⚠️ **Đã đổi cách tiếp cận** (bản đầu dùng route one-off kiểu `addCurrencyList()`, xem lịch sử bên dưới) — thực tế kiểm tra lại thấy route one-off **rất dễ bị quên gọi**: đã xảy ra ngay trên DB dev (TODO đánh dấu "đã seed, id=853" nhưng DB thực tế không có row đó, do route chưa từng được hit trên DB này). Theo yêu cầu của user, đổi sang **Doctrine migration** — mỗi lần deploy đều chạy `doctrine:migrations:migrate` nên insert luôn được đảm bảo chạy đúng 1 lần, không cần nhớ gọi endpoint riêng. File: `migrations/Version20260818090000.php`, có `WHERE NOT EXISTS (...)` để idempotent nếu lỡ chạy lại.

```php
// migrations/Version20260818090000.php
public function up(Schema $schema): void
{
    $this->addSql(<<<SQL
        INSERT INTO status_list (id, name, type, position, is_active, unique_key, date_created, date_updated)
        SELECT nextval('my_status_list_id_seq'), 'Hämtas i gällstad', 'order_delivery_type', 0, true, 'delivery_type_hamtas_i_gallstad', NOW(), NOW()
        WHERE NOT EXISTS (
            SELECT 1 FROM status_list
            WHERE type = 'order_delivery_type'
              AND unique_key = 'delivery_type_hamtas_i_gallstad'
              AND date_deleted IS NULL
        )
    SQL);
}
```

Đã chạy `doctrine:migrations:migrate` trên DB dev, verify: insert thành công, `id = 856`.

`position = 0` (không phải 4 như bản đầu) — vì check lại DB thấy cả 4 row có sẵn (`Annan leveranssätt`, `Bulk`, `Direkt leverans`, `Delleverans`) đều có `position = 0`, field này không thực sự dùng để sort thứ tự hiển thị cho group `order_delivery_type`, nên giữ đồng nhất `= 0` thay vì tự suy đoán 1 con số khác.

`StatusListService::addDeliveryTypeHamtasIGallstad()`, route `api_other_add_delivery_type_hamtas_i_gallstad`, và `OtherController::addDeliveryTypeHamtasIGallstad()` đã bị **xoá** (không cần endpoint one-off không có auth check nữa).

### 2. Chặn tạo label ở `PostNordService::orderPostnordCreateLabel()`

File: `src/Service/PostNordService.php:487`

Danh sách `unique_key` bị chặn đặt thành 1 const dùng chung, ở `StatusList` entity (để không lặp lại giữa `PostNordService` và `OrderService` — xem mục 3):

```php
// src/Entity/StatusList.php
const DELIVERY_TYPE_KEYS_BLOCK_POSTNORD = [
    'delivery_type_bulk',                  // A
    'delivery_type_hamtas_i_gallstad',      // B (mới)
    'delivery_type_annan_leveranssatt',     // C
];
```

Thêm check ngay sau đoạn tìm `$order` (trước đoạn tìm `$channel`, khoảng dòng 497):

```php
if ($order->getDeliveryTypeId()) {
    $deliveryType = $this->em->getRepository(StatusList::class)->findOneBy([
        'id' => $order->getDeliveryTypeId(),
        'type' => StatusList::TYPE_ORDER_DELIVERY_TYPE,
    ]);
    if ($deliveryType && in_array($deliveryType->getUniqueKey(), StatusList::DELIVERY_TYPE_KEYS_BLOCK_POSTNORD)) {
        return [
            'status_code' => 400,
            'data' => [
                'message' => 'Postnord fraktetikett kan inte skapas för denna leveranssätt.',
            ]
        ];
    }
}
```

#### ⚠️ Lưu ý bẫy quan trọng

`Order::getDeliveryTypeId()` chỉ set khi FE gửi `deliveryTypeId` lúc tạo/sửa order (`OrderService.php:469-482`) — order cũ chưa từng set delivery type sẽ có `deliveryTypeId = null`, không bị chặn (cho phép tạo label như bình thường, đúng hành vi hiện tại). Chỉ chặn khi **có** delivery type và nó thuộc danh sách blocked.

### 3. Field `allowPostNord` trên response order detail — cho frontend show/hide box Postnord

Thay vì để FE tự so `uniqueKey`, API trả sẵn field boolean `allowPostNord` trong response order (cả `GET /api/v1/orders/{id}` và list), tính bằng cùng 1 logic/const với check ở mục 2 — đảm bảo FE luôn đồng bộ với backend, không cần biết danh sách `unique_key` bị chặn là gì.

- File: `src/Service/OrderService.php` — helper `isPostNordAllowed(Order $order): bool` (đặt trước `generateItemForList()`), gọi trong `generateItemForList()` ngay cạnh `deliveryType`/`deliveryTypeId`:
  ```php
  'deliveryType' => $order->getDeliveryType(),
  'deliveryTypeId' => $order->getDeliveryTypeId(),
  'allowPostNord' => $this->isPostNordAllowed($order),
  ```
- Field này có mặt ở **mọi** response order (cả list và detail) vì được set trong `generateItemForList()`, dùng chung bởi cả `generateItem()` và `generateItemNewList()`.
- FE chỉ cần: `if (!order.allowPostNord) { /* ẩn box Postnord fraktetikett */ }`.
- Nếu FE vẫn gọi `POST /{id}/postnord/create-label` khi `allowPostNord = false` (bypass UI), API vẫn trả `400` với message ở mục 2 — không thể bypass qua gọi API trực tiếp.

#### ⚠️ Optimize N+1: cache blocked delivery-type IDs 1 lần/request

`OrderService::list()` (dòng 599) gọi `generateItemNewList()` trong vòng `foreach` cho tối đa 100 order/trang (`$criteria['limit'] ?? 100`). Nếu `isPostNordAllowed()` query `status_list` mỗi lần gọi thì list 1 trang order sẽ tốn thêm tới 100 query dư thừa.

→ Đã tối ưu: `isPostNordAllowed()` chỉ query `status_list` **1 lần duy nhất mỗi request** (lazy-load, cache vào property instance `$blockedPostNordDeliveryTypeIds`), sau đó so sánh bằng `in_array()` trong memory cho tất cả order còn lại trong cùng request:

```php
private $blockedPostNordDeliveryTypeIds; // property mới, cạnh $uow

private function isPostNordAllowed(Order $order): bool
{
    if (!$order->getDeliveryTypeId()) {
        return true;
    }
    if ($this->blockedPostNordDeliveryTypeIds === null) {
        $rows = $this->em->getRepository(StatusList::class)->findBy([
            'type' => StatusList::TYPE_ORDER_DELIVERY_TYPE,
            'uniqueKey' => StatusList::DELIVERY_TYPE_KEYS_BLOCK_POSTNORD,
        ]);
        $this->blockedPostNordDeliveryTypeIds = array_map(fn($row) => $row->getId(), $rows);
    }
    return !in_array($order->getDeliveryTypeId(), $this->blockedPostNordDeliveryTypeIds);
}
```

Đã verify: gọi `generateItem()` lặp lại nhiều lần trong cùng 1 process (giống hành vi trong `list()`) cho kết quả đúng, chỉ dùng 1 query cache.

### 4. `bulkOrderPostnordCreateLabel()` — đã xác nhận KHÔNG cần thêm check tương tự

Đã kiểm tra `src/Entity/BulkOrder.php`: entity này **không có field `deliveryType`/`deliveryTypeId`** nào cả — không áp dụng logic Leveranssätt A/B/C. Ngược lại, khi 1 order được gán vào 1 bulk order (`BulkOrderService.php:456-471`), chính order đó tự động bị set `deliveryTypeId` = id của "Bulk" (`delivery_type_bulk`). Nghĩa là guard mới ở mục 2 **đã tự động chặn đúng** các order này khi ai đó cố gọi `orderPostnordCreateLabel()` riêng lẻ cho từng order thay vì dùng flow bulk shipping (`bulkOrderPostnordCreateLabel()`) — hành vi này khớp với business logic hiện có, không cần code thêm.

---

## TODO List

```
### Backend — Data (StatusList)
- [x] Xác nhận tên hiển thị + unique_key chính thức cho option "Hämtas i gällstad" với PO (xem Question #1) — dùng đúng đề xuất
- [x] Đổi seed sang Doctrine migration `migrations/Version20260818090000.php` (idempotent, `WHERE NOT EXISTS`) — thay cho route one-off (dễ bị quên gọi, không có auth check); migration thì luôn chạy khi deploy chạy `doctrine:migrations:migrate`, không cần thêm bước thủ công
- [x] Chạy migration trên DB dev hiện tại — đã insert, `id = 856`, verify bằng `SELECT * FROM status_list WHERE type = 'order_delivery_type'`. Production sẽ tự có row này khi chạy `doctrine:migrations:migrate` lúc deploy (quy trình deploy hiện tại luôn chạy migration), không cần thao tác gì thêm.

### Backend — Service (chặn tạo label + expose field cho FE)
- [x] `src/Entity/StatusList.php` — thêm const dùng chung `DELIVERY_TYPE_KEYS_BLOCK_POSTNORD`
- [x] `src/Service/PostNordService.php` — check trong `orderPostnordCreateLabel()` (dòng ~487), dùng `StatusList::DELIVERY_TYPE_KEYS_BLOCK_POSTNORD`
- [x] `src/Service/OrderService.php` — thêm helper `isPostNordAllowed()`, expose field `allowPostNord` trong `generateItemForList()` (áp dụng cho mọi response order, cả list và detail)
- [x] Optimize `isPostNordAllowed()` để không N+1 query trong `list()` — cache blocked IDs 1 lần/request (xem mục 3, phần "Optimize N+1")
- [x] Kiểm tra `bulkOrderPostnordCreateLabel()` có cần check tương tự không → **không cần**, `BulkOrder` không có field delivery type, và order con trong bulk order đã tự bị chặn qua guard ở mục 2 (xem mục 4)

### Frontend — React *(ngoài repo này, chỉ note lại cho FE team)*
- [ ] Dropdown Leveranssätt hiển thị thêm option "Hämtas i gällstad" (tự động có sau khi BE seed xong, không cần đổi code FE nếu dropdown đang load động từ API)
- [ ] Ẩn khối "Postnord fraktetikett" dựa theo field `allowPostNord` (boolean) có sẵn trong response order — không cần tự so `uniqueKey`
- [ ] Handle message lỗi 400 trả về từ `POST /{id}/postnord/create-label` nếu vẫn lỡ gọi khi bị chặn

### Test / kiểm tra
- [x] Seed xong, DB có đủ 5 option cho `order_delivery_type` (Direkt leverans, Delleverans, Bulk, Hämtas i gällstad, Annan leveranssätt) — verify qua SQL
- [x] Order thật có `delivery_type_id = 285` (Bulk, order id 4630) → gọi trực tiếp `PostNordService::orderPostnordCreateLabel()` → nhận `400` với message chặn, không gọi tới PostNord API (test qua command tạm, đã xoá sau khi test)
- [x] `OrderService::generateItem()` cho order 4630 (Bulk) → `allowPostNord = false`; order 3439 (`deliveryTypeId = null`) → `allowPostNord = true` (test qua command tạm, đã xoá sau khi test)
- [ ] Test qua API thật (`GET /api/v1/orders/{id}` và `POST /api/v1/orders/{id}/postnord/create-label` với Bearer token) cho cả 3 case A/B/C
- [ ] Order chọn delivery type = Direkt leverans/Delleverans → `allowPostNord = true`, tạo label bình thường (không bị chặn nhầm) — chưa test qua API thật, đã verify qua logic code
```

---

## Các file/files liên quan

| File | Mục đích |
|------|----------|
| `src/Entity/StatusList.php` | Const `TYPE_ORDER_DELIVERY_TYPE`, `DELIVERY_TYPE_KEYS_BLOCK_POSTNORD` (mới), field `uniqueKey` |
| `migrations/Version20260818090000.php` | Insert StatusList entry "Hämtas i gällstad" (idempotent, `WHERE NOT EXISTS`) — thay cho route one-off cũ |
| `src/Service/PostNordService.php` | `orderPostnordCreateLabel()` dòng 487 — check chặn dùng `StatusList::DELIVERY_TYPE_KEYS_BLOCK_POSTNORD` |
| `src/Service/OrderService.php` | `isPostNordAllowed()` (mới, helper), field `allowPostNord` trong `generateItemForList()`; dòng 469-482 (gán `deliveryType`/`deliveryTypeId` khi update order), dòng 1044-1053 (default delivery type = Annan leveranssätt khi `customerDeliveryAddressId = -1`) |
| `src/Service/BulkOrderService.php` | Dòng 456-471 — khi gán order vào bulk order, tự động set `deliveryTypeId` = "Bulk" cho order đó (lý do guard mới tự động áp dụng đúng cho order thuộc bulk order, xem mục 4) |
| `src/Entity/BulkOrder.php` | Đã kiểm tra: không có field delivery type, `bulkOrderPostnordCreateLabel()` không cần check tương tự |
| `src/Application/ApiBundle/Controller/OrderController.php` | Route `postnord/create-label`, `postnord/show-label` (dòng 335, 348) |
