# TSHIRTORDER-1603 — Support channel specific PostNord account

## Yêu cầu gốc

> Add field on each channel, by default use the one tshirt today, add checkbox to activate SUB account
>
> "postnord_api_key": "xxyyzz...",             // API-nyckel (om kunden använder eget API-konto)
> "postnord_customer_number": "1234567890",    // PostNords kundnummer (Obligatoriskt)

---

## Tổng quan

Hiện tại toàn bộ integration PostNord (tạo fraktetikett/label, lấy PDF label) dùng **1 tài khoản PostNord chung** (global, cấu hình trong `config/services.yaml`: `postnord_api_key`, `postnord_consignor_party_id`) cho mọi channel — đây là tài khoản "tshirt today".

Task này thêm khả năng **1 channel dùng tài khoản PostNord riêng** (sub account) của chính channel đó, thay vì dùng chung tài khoản global. Mặc định (checkbox tắt) mọi channel vẫn dùng tài khoản global "tshirt today" như hiện nay — không ảnh hưởng hành vi cũ.

Phạm vi: Backend (API). Không có UI mockup — field sẽ được thêm vào form cấu hình Channel hiện có ở FE (repo riêng), giống pattern các field Svea (`sveaAllowImport`, `sveaAccessToken`, ...) đã có.

---

## Chi tiết kỹ thuật

### Field mapping với yêu cầu gốc

| Field yêu cầu | Field trong hệ thống | Ghi chú |
|---|---|---|
| checkbox "activate SUB account" | `Channel::postnordUseSubAccount` (boolean) | Bật = dùng key/customer number riêng của channel |
| `postnord_api_key` | `Channel::postnordApiKey` (string) | Thay cho global `postnord_api_key` (`config/services.yaml`) khi bật sub account |
| `postnord_customer_number` | `Channel::postnordCustomerNumber` (string) | Thay cho global `postnord_consignor_party_id` — field này hiện đang được set vào `consignor.partyIdentification.partyId` khi tạo shipment (`PostNordService::orderCreateLabelGenerateShipmentData()` / `bulkOrderCreateLabelGenerateShipmentData()`) |

Base URL (`postnord_api_url`, `postnord_api_pdf_url`, test/live switch) **vẫn dùng chung global** — không có theo channel, vì mọi tài khoản (kể cả sub account) dùng chung 1 environment (test hoặc live) như config hiện tại.

### Logic fallback

`PostNordService` có 2 helper method mới:
- `getPostnordApiKey(Channel $channel): string` — trả về `$channel->getPostnordApiKey()` nếu `postnordUseSubAccount = true` **và** field không rỗng, ngược lại trả về global `postnord_api_key` từ `parameterBag`.
- `getPostnordConsignorPartyId(Channel $channel): string` — tương tự, dùng `$channel->getPostnordCustomerNumber()`.

Áp dụng ở tất cả nơi đang gọi PostNord API theo channel:
- `curlCreateLabel()` — nhận thêm tham số `Channel $channel`, dùng `getPostnordApiKey()`
- `orderCreateLabelGenerateShipmentData()` / `bulkOrderCreateLabelGenerateShipmentData()` — dùng `getPostnordConsignorPartyId()` thay vì đọc thẳng `parameterBag`
- `orderPostnordShowLabel()` / `bulkOrderPostnordShowLabel()` (lấy PDF label) — trước đây không load `Channel`, giờ load thêm channel từ `$order->getChannelId()` để lấy đúng `apiKey`

---

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

### Entity: `Channel`
**File:** `src/Entity/Channel.php`

| Field | Type | Default | Ý nghĩa |
|---|---|---|---|
| `postnordUseSubAccount` | boolean, nullable | `false` | Bật/tắt dùng tài khoản PostNord riêng của channel |
| `postnordApiKey` | string(255), nullable | `null` | API key PostNord riêng của channel |
| `postnordCustomerNumber` | string(255), nullable | `null` | Customer number / party ID PostNord riêng của channel |

Migration: `migrations/Version20260818073419.php` — đã chạy migrate thành công ngày 2026-08-18.
```sql
ALTER TABLE channels ADD postnord_use_sub_account BOOLEAN DEFAULT NULL;
ALTER TABLE channels ADD postnord_api_key VARCHAR(255) DEFAULT NULL;
ALTER TABLE channels ADD postnord_customer_number VARCHAR(255) DEFAULT NULL;
```

> ⚠️ **Lưu ý migration:** `doctrine:migrations:diff` cũng phát hiện schema drift không liên quan (bảng `activity_log` đang partitioned theo tháng ở DB nhưng entity map vào 1 bảng đơn — drift có từ trước, không phải do task này). Đã **loại bỏ thủ công** phần SQL đó khỏi migration này để tránh drop nhầm các bảng partition đang có dữ liệu. Nếu cần dọn drift đó thì làm ở 1 migration/task riêng.

### API: `PATCH /api/v1/channels/{id}` (route cập nhật channel hiện có)
**File:** `src/Application/ApiBundle/Controller/ChannelController.php` → `ChannelService::update()`

Request body thêm 3 field (đều optional, cùng convention với field boolean khác — FE gửi `"true"`/`"false"` string hoặc bool đều được, `ChannelService::generateUpdate()` tự convert):
```json
{
  "postnordUseSubAccount": true,
  "postnordApiKey": "xxyyzz...",
  "postnordCustomerNumber": "1234567890"
}
```

Response `GET /api/v1/channels/{id}` (qua `ChannelService::generateItem()`) trả về thêm:
```json
{
  "postnordUseSubAccount": false,
  "postnordApiKey": null,
  "postnordCustomerNumber": null
}
```

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

Nếu FE bật `postnordUseSubAccount = true` nhưng để trống `postnordApiKey`/`postnordCustomerNumber`, hệ thống **tự động fallback về tài khoản global** (không lỗi, không tạo request PostNord với key rỗng) — vì logic check `!empty($channel->getPostnordApiKey())` trước khi dùng giá trị channel. Nên nhắc FE validate bắt buộc nhập đủ 2 field khi tick checkbox, để tránh user tưởng đã đổi tài khoản nhưng thực ra vẫn đang dùng tài khoản chung.

---

## TODO List

```
### Backend — Entity & Migration
- [x] Thêm field `postnordUseSubAccount`, `postnordApiKey`, `postnordCustomerNumber` vào `src/Entity/Channel.php` (+ getter/setter)
- [x] Generate + chạy migration `Version20260818073419`

### Backend — Service
- [x] `ChannelService::generateItem()` — trả 3 field mới
- [x] `ChannelService::generateUpdate()` — thêm `postnordUseSubAccount` vào `$booleanFields`
- [x] `PostNordService`: thêm `getPostnordApiKey()`, `getPostnordConsignorPartyId()`, áp dụng vào `curlCreateLabel()`, `orderCreateLabelGenerateShipmentData()`, `bulkOrderCreateLabelGenerateShipmentData()`, `orderPostnordShowLabel()`, `bulkOrderPostnordShowLabel()`

### Frontend — React *(repo riêng, không nằm trong repo này)*
- [ ] Thêm checkbox "Use own PostNord account (SUB account)" trong form cấu hình Channel
- [ ] Khi bật checkbox → hiện 2 input `postnordApiKey`, `postnordCustomerNumber` (nên bắt buộc nhập khi checkbox bật, xem lưu ý bẫy ở trên)

### Test / kiểm tra
- [ ] Test tạo label PostNord cho channel **không** bật sub account → vẫn dùng tài khoản global như cũ (regression)
- [ ] Test tạo label PostNord cho channel **có** bật sub account + đủ key/customer number → gọi API PostNord bằng key riêng, `consignor.partyIdentification.partyId` = customer number riêng
- [ ] Test bật sub account nhưng để trống key/customer number → tự fallback về global, không lỗi
- [ ] Test lấy PDF label (`orderPostnordShowLabel`, `bulkOrderPostnordShowLabel`) dùng đúng key theo channel
```

---

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

| File | Mục đích |
|------|----------|
| `src/Entity/Channel.php` | 3 field mới + getter/setter |
| `src/Service/ChannelService.php` | Đọc/ghi field mới (`generateItem`, `generateUpdate`) |
| `src/Service/PostNordService.php` | Logic fallback channel → global khi gọi PostNord API |
| `migrations/Version20260818073419.php` | Migration thêm cột |
| `config/services.yaml` | Giá trị global mặc định (`postnord_api_key`, `postnord_consignor_party_id`) — không đổi |
