# TSHIRTORDER-1644 — Tích hợp NWG (Merch): nhập đơn từ FTP + gửi trạng thái qua API

## Yêu cầu gốc

> TSHIRTORDER-1644 NEW integration - NWG we setup FTP already here is the mapping for the fields
> original description in SV in PDF
>
> 1. Product, supplier SKU, and name in the file
> This has been arranged. Each order will now come with an order file, Order-{ordernumber}.csv, located in the order's folder on the FTP alongside the work order and print files. It contains which garment you should use (Supplier SKU), the product name, and our SKU.
>
> 2. Order number
> Yes, SP0590213353056447 is our order number. It is the name of the folder on the FTP, it appears on the work order, and it is listed on every row in the order file. This is the number you map against.
>
> 3. Order format
> The order file contains one row per product, and the order number is repeated on each row: Order number | Line | SKU | Supplier SKU | Product | Size | Quantity | Design version
> SKU is our item code, i.e., what the customer ordered.
> Supplier SKU is the garment you take from your inventory.
> The file is semicolon-separated and encoded in UTF-8. Let me know if you prefer a different format or additional columns.
>
> 4. Statuses
> You send status updates to our API per order using our order number:
> POST /printer/orders/{ordernumber}/status
> We will provide you with a key that you must include in every request header under x-functions-key. I will send the address and the key separately.
> Statuses: InProduction: The order is in production. Shipped: The order, or part of it, has been shipped. Cancelled: The order cannot be fulfilled.
> Examples:
> `{ "status": "InProduction" }`
> `{ "status": "Shipped", "shipment": { "carrier": "PostNord", "trackingNumber": "00370712345678901234", "trackingUrl": "https://..." } }`
> `{ "status": "Cancelled", "message": "Reason" }`
> Entire order in one package: Send Shipped with the tracking number, without specifying lines. Multiple packages: Send one Shipped call per package and specify what the package contains, including SKU (ours or supplier's) and quantity:
> `"lines": [ { "sku": "3410010-100-5", "quantity": 2 } ]`
> The order will be considered partially shipped until everything is delivered. Duplicate calls: Sending the same call multiple times won't cause any issues, so you can retry if you are unsure whether it was received. Non-cancellable: An order that has started shipping cannot be cancelled. Let me know if any of this does not suit your system.
>
> 5. Recurring SKUs
> Every row in the order file includes a Designversion, a code generated based on the print details: print file, technology, placement, size, and font/color for names and numbers. As long as the print details remain identical, the code stays the same across orders and sizes. If anything in the print changes, the line receives a new code, indicating that the product needs to be updated on your end.

Bản gốc tiếng Thụy Điển: email của Robin Persson (NWG) gửi Liem ngày 05/10/2026, xem `files/nwg-email-2026-10-05.pdf`. Ảnh chụp các ví dụ JSON: `files/status-examples-screenshot.png`.

Bộ file mẫu của đơn `SPO590213353056447` (lấy từ `temp_files/Merch-exempelorder-SPO590213353056447`), đã copy vào `files/`:

| File | Nội dung |
|---|---|
| `Arbetsorder-SPO590213353056447.pdf` | Phiếu sản xuất: ngày đặt, mã tham chiếu của khách, **địa chỉ giao hàng**, từng dòng kèm thông tin in (kỹ thuật, mặt in, file, kích thước, vị trí, font/màu), danh sách tên/số, ảnh preview |
| `Namnlista.csv` | Tên/số in theo từng cái áo: `Rad;Plagg;Storlek;Nr;name;number` |
| `404f97ec-test.png` | File in (DTF) mặt trước |
| `3410010-100-front-0-preview.png`, `3410010-100-back-0-preview.png` | Ảnh preview mặt trước/sau |

⚠️ **Bộ mẫu chưa có `Order-SPO590213353056447.csv`**, là file chính của tích hợp. Sếp đang chờ NWG gửi thêm file test.

---

## Tổng quan

Tích hợp với đối tác mới **NWG** (shop merch, người liên hệ: Robin Persson). NWG là khách hàng, bên mình là xưởng in (POD). Tích hợp có 2 chiều:

1. **Chiều vào (Cronjob):** NWG tạo **một folder cho mỗi đơn** trên FTP, tên folder là mã đơn. Mình đọc `Order-{ordernumber}.csv` cùng các file đi kèm (phiếu sản xuất PDF, Namnlista, file in, preview), rồi tạo `Order` + `OrderProduct` trong channel NWG. Cách làm giống import PINshirt (TSHIRTORDER-1627).
2. **Chiều ra (gọi HTTP sang NWG):** khi đơn đổi trạng thái thì gửi `POST /printer/orders/{ordernumber}/status` sang API của NWG (`InProduction` / `Shipped` + tracking / `Cancelled`). Phần này **chưa có tiền lệ** trong codebase với đúng kiểu này. Gần nhất là các luồng resync khi đơn Complete (Shopify fulfillment, Deco, WP) trong `OrderService::postOrderUpdateComplete()`.

Phạm vi: chủ yếu backend (Cronjob + Service + entity/migration cho trạng thái đồng bộ). Frontend có thể cần nút gửi lại hoặc hiển thị trạng thái đồng bộ (repo React, chờ chốt).

### ⚠️ Phát hiện quan trọng: nhiều phần đã có sẵn từ PINshirt / PostNord

| Hạng mục | File | Trạng thái |
|---|---|---|
| Khung import qua SFTP: storage interface (SFTP + local để test), option `--local-dir/--dry-run/--parse-only/--no-archive/--dump`, mỗi order chạy trong transaction riêng, kiểm tra số item đã lưu | `src/Command/Cronjob/ImportOrderPinshirtCommand.php`, `src/Service/Pinshirt/*` | ✅ Có mẫu để làm theo. Khác ở chỗ PINshirt là **1 file CSV chứa nhiều order**, còn NWG là **1 folder cho mỗi order** |
| Tìm/tạo channel theo `synId`, tìm/tạo customer, product theo SKU, sub-product theo size, đính file vào Korrektur | `PinshirtOrderImporter::findOrCreateChannel/Customer/Product/SubProduct`, `buildUploads` | ✅ Có mẫu |
| Credential SFTP dùng chung `INTEGRATION_FTP_*` (param `integration_ftp_*`) | `config/services.yaml:23-32`, `.env:55-58` | ✅ Có. Dùng được nếu NWG upload lên cùng server `integration` (chờ xác nhận) |
| Field theo từng dòng `OrderProduct.modelSku` / `modelName` (mẫu áo phôi, TSHIRTORDER-1642) và `supplierSku` / `supplierColor` (TSHIRTORDER-1616, nhập tay) | `src/Entity/OrderProduct.php` | ✅ Có. Chưa chốt cột `Leverantör SKU` vào field nào |
| Lưu tracking PostNord khi tạo label: `postNordShipmentId`, `postNordTrackingUrl` (chỉ của kolli đầu tiên); tất cả kolli lưu serialize trong `postNordLatestData` | `src/Service/PostNordService.php:517-672` | ✅ Có |
| Tạo label PostNord → order chuyển sang **Complete** → gọi `postOrderUpdateComplete()` (rẽ nhánh theo `createdFrom`: Svea, `shopify`, `deco`, còn lại là WP resync) | `PostNordService.php:595-620`, `OrderService.php:4770` | ✅ Có. Đây là chỗ móc lệnh gửi `Shipped` |
| Mẫu "đánh dấu cần resync + ngày resync" để cron gửi lại khi lỗi | `Order::$decoNeedResync`, `$decoResyncDate` | ✅ Có mẫu |
| Cronjob gửi lại đơn đã Complete | `src/Command/Cronjob/ResyncWpCompletedOrderCommand.php` | ✅ Có mẫu |
| Chức năng huỷ đơn (TSHIRTORDER-1576) | `docs/issues/TSHIRTORDER-1576-cancellation-function.md` | ✅ Có, cần đọc lại khi móc lệnh gửi `Cancelled` |
| Command import NWG, client gọi API trạng thái NWG, field lưu trạng thái đã gửi | — | ❌ Chưa có, cần làm |

---

## Luồng hoạt động

### 1. Cấu trúc FTP (theo email; chưa thấy trên server)

```
{root NWG}/
├── SPO590213353056447/                 ← 1 folder = 1 đơn, tên = mã đơn
│   ├── Order-SPO590213353056447.csv    ← file chính (chưa có mẫu)
│   ├── Arbetsorder-SPO590213353056447.pdf
│   ├── Namnlista.csv                   ← chỉ có khi đơn có in tên/số? (chờ xác nhận)
│   ├── 404f97ec-test.png               ← file in
│   ├── 3410010-100-front-0-preview.png
│   └── 3410010-100-back-0-preview.png
└── SPO.../
```

Chưa biết: path gốc trên server, server nào (có phải `161.35.83.188` / tài khoản `integration` như Nakata/PINshirt không), và sau khi xử lý thì folder có được chuyển sang `archived/` không (xem Pending #6, #7).

### 2. Import đơn (cron, đề xuất `order:import_nwg`)

1. Liệt kê các folder con trong root NWG (bỏ `archived/`).
2. Với mỗi folder: **chỉ xử lý khi đã có `Order-{folder}.csv`**. Chưa có thì bỏ qua và để lần chạy sau (có thể NWG đang upload dở; chờ xác nhận thứ tự upload, Pending #7).
3. Đọc CSV: phân cách `;`, UTF-8, **strip BOM**, `trim` cả key lẫn value kể cả `\r` (bài học từ PINshirt). Đọc theo **tên cột**. Header là tiếng Thụy Điển (`Ordernummer;Rad;SKU;Leverantör SKU;Produkt;Storlek;Antal;Designversion`) hay tiếng Anh: chờ file mẫu (Pending #1).
4. Kiểm tra: mọi dòng có cùng `Ordernummer` và trùng tên folder. Lệch thì coi là lỗi cấp folder: log critical, không import.
5. Đọc `Namnlista.csv` nếu có, nối vào item theo `Rad`.
6. Import 1 order trong transaction riêng (giống `PinshirtOrderImporter::import()`): tìm/tạo channel `nwg`, tìm order theo `orderNr` + `channelId`, tạo mới hoặc xử lý đơn đã tồn tại (Pending #10).
7. Đính kèm file vào Korrektur của order: file in PNG, preview, PDF phiếu sản xuất (Pending #5).
8. Xong thì archive folder (Pending #6) và ghi log vào file log riêng (Monolog channel `nwg`, giống `pinshirt`).

### 3. Mapping field (đề xuất; các cột CSV theo bảng trong email)

Theo quy ước đã chốt (memory "literal field mapping"): **cột mới của file import thì map 1:1 vào field mới, không diễn giải lại**. Phần cần sếp chốt được đánh dấu *(chờ #...)*.

#### `orders`

| Nguồn | Cột | Ghi chú |
|---|---|---|
| `Ordernummer` (= tên folder) | `orders.order_nr` | Khoá để map và để gọi API trạng thái. **Phải giữ nguyên chính xác**, xem Pending #2 (chữ `O` hay số `0`) |
| hằng số `'import_nwg'` | `orders.created_from` | Dùng để rẽ nhánh trong `postOrderUpdateComplete()` |
| channel `synId = 'nwg'` | `orders.channel_id` | Tìm theo `synId`, chưa có thì tạo mới như PINshirt (đã chốt, Questions #4) |
| — | `orders.customer_id` | Customer là ai: NWG (1 customer cố định) hay người nhận? *(chờ #8)* |
| **địa chỉ giao hàng** | `orders.name`, `delivery_address`, `delivery_post_nr`, `delivery_city`, `delivery_country` | ❌ **CSV không có**, chỉ có trong PDF *(chờ #3, blocker)* |
| ngày đặt (`Beställd`), mã tham chiếu (`Kundens referens`) | `orders.date_order`, ? | ❌ Chỉ có trong PDF *(chờ #3)* |
| `status_list` (`order_status_web_id`) | `orders.order_status_id` | Như PINshirt *(chờ #9)* |

#### `orders_products` (mỗi dòng CSV = 1 item)

| Cột CSV | Cột DB | Ghi chú |
|---|---|---|
| `Rad` | `orders_products.item_id` | Như PINshirt (`line_no`); dùng để nối với `Namnlista.csv` |
| `SKU` (vd `3410010001-100-5`, mã hàng của NWG) | ? | *(chờ #4)*: SKU của product hay field mới? |
| `Leverantör SKU` (vd `3410010-100-5`, áo phôi lấy từ kho mình) | ? | *(chờ #4)*: `modelSku` (TSHIRTORDER-1642), `supplierSku` (TSHIRTORDER-1616) hay field mới? Cũng là SKU mình gửi lại trong `lines` khi ship từng phần |
| `Produkt` (vd `Core Unify Tee`) | `orders_products.product_name` | Như PINshirt |
| `Storlek` (vd `M`, `L`) | `sub_product_id`, `product_size_id`, `product_size_text` | Tìm/tạo sub-product theo size như PINshirt |
| `Antal` | `orders_products.quantity` | |
| `Designversion` (vd `3fa9c01b2e7d`) | field mới? | *(chờ #11)* |
| Namnlista `name`, `number` theo từng áo | ? | *(chờ #12)* |
| Thông tin in (kỹ thuật, mặt, kích thước, vị trí, font/màu) | `orders_products.comment`? | ❌ Chỉ có trong PDF *(chờ #5)* |
| — (CSV không có giá) | `orders_products.price` | Giá của product, product mới tạo thì `0` như PINshirt *(chờ #13)* |

### 4. Gửi trạng thái sang NWG

Endpoint: `POST {NWG_API_BASE_URL}/printer/orders/{orderNr}/status`, header `x-functions-key: {NWG_API_KEY}`, `Content-Type: application/json`. Base URL và key NWG sẽ gửi riêng. Đề xuất để trong `.env` (URL) và `.env.local` (key, không commit), inject qua `ParameterBagInterface`.

| Trạng thái NWG | Body | Khi nào gửi (đề xuất, *chờ #9*) |
|---|---|---|
| `InProduction` | `{ "status": "InProduction" }` | Khi order chuyển sang status sản xuất (status nào?) hoặc ngay sau khi import thành công |
| `Shipped` (cả đơn 1 kiện) | `{ "status": "Shipped", "shipment": { "carrier": "PostNord", "trackingNumber": ..., "trackingUrl": ... } }` | Trong `postOrderUpdateComplete()` khi `createdFrom == 'import_nwg'`, tức sau khi tạo label PostNord. `trackingNumber` = `postNordShipmentId`, `trackingUrl` = `postNordTrackingUrl` |
| `Shipped` (nhiều kiện) | như trên + `"lines": [ { "sku": ..., "quantity": ... } ]` | *(chờ #14)* |
| `Cancelled` | `{ "status": "Cancelled", "message": "..." }` | Khi huỷ order (luồng TSHIRTORDER-1576). Lý do lấy từ đâu? Không được huỷ khi đã gửi `Shipped` |

**Retry:** NWG nói gửi trùng không sao, nên có thể gửi lại thoải mái. Đề xuất làm giống Deco: lưu trạng thái gửi trên order (cần gửi lại không, lần gửi cuối, lỗi cuối) và có cron gửi lại các lệnh lỗi. Mọi request/response ghi vào log `nwg`.

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

- **Nhiều kolli PostNord:** `orderPostnordCreateLabel()` tạo `nr_of_kolli` label, nhưng order chỉ lưu tracking của **kolli đầu tiên** (`i == 0`); các kolli còn lại chỉ nằm trong `postNordLatestData` (serialize). Hệ thống cũng **không biết kolli nào chứa dòng nào**. Vì vậy chưa gửi được `Shipped` theo từng kiện có `lines`. Đề xuất: luôn gửi 1 lệnh `Shipped` cho cả đơn, không kèm `lines` (Pending #14).
- **`postOrderUpdateComplete()`** rẽ nhánh theo `createdFrom`. Nếu không thêm nhánh `import_nwg` trước nhánh WP, đơn NWG sẽ rơi vào nhánh WP và trả 400 `Resync url is empty` (không gây lỗi nhưng không gửi gì). Hàm này còn được gọi khi user đổi status sang Complete trên UI (`OrderService.php:1526`) và từ `ResyncWpCompletedOrderCommand`. Trường hợp complete mà **không tạo label PostNord** (không có tracking) thì gửi gì? (Pending #9)
- **Gọi HTTP trong lúc tạo label:** nếu API NWG chậm hoặc lỗi thì không được làm hỏng việc tạo label. Gọi có timeout ngắn, bắt mọi lỗi, đánh dấu để cron gửi lại.
- **Mã đơn là khoá:** `orderNr` phải giữ nguyên ký tự đúng như tên folder (xem Pending #2), không `trim`/đổi hoa thường tuỳ tiện, và phải URL-encode khi ghép vào path.
- **Bài học từ PINshirt:** `addProducts()` im lặng bỏ qua item lỗi, nên sau `add()` phải so số item đã lưu với số dòng CSV. Repository dùng `LIKE` nên phải so khớp lại bằng `strcasecmp` (`firstExact`). `FileService::uploadFile()` cần `UploadedFile` (test mode).

---

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

| # | Câu hỏi | Trả lời | Ghi chú kỹ thuật |
|---|---------|---------|-------------------|
| 1 | Mã đơn NWG dùng làm khoá? | Có: là tên folder, có trên phiếu sản xuất và trên mọi dòng CSV (email, điểm 2) | Map vào `orders.order_nr` |
| 2 | Định dạng CSV? | `;`, UTF-8, 1 dòng/sản phẩm, mã đơn lặp lại ở mỗi dòng (email, điểm 3) | |
| 3 | Gửi trùng lệnh trạng thái có sao không? | Không sao, retry được (email, điểm 4) | Cho phép cron gửi lại không cần chống trùng |
| 4 | Channel NWG? (Pending #8) | **Tạo channel mới nếu chưa có channel `synId = 'nwg'`** (Kevin, 06/10/2026) | Giống `PinshirtOrderImporter::findOrCreateChannel()`: tìm theo `synId` + `dateDeleted IS NULL`, chưa có thì `ChannelService::add()` với `synId = 'nwg'`, tên `NWG`, `active = true`. Phần customer của #8 vẫn còn chờ |
| 5 | Header CSV, mã đơn `SPO…`/`SP0…`? (Pending #1, #2) | **Chờ file mẫu** `Order-….csv` (Kevin, 06/10/2026) | Chưa code phần đọc CSV cho tới khi có file mẫu |
| 6 | FTP (Pending #7)? | **Chờ**, sẽ có sau (Kevin, 06/10/2026) | Trước mắt phát triển và test bằng `--local-dir`, không phụ thuộc SFTP |

---

## Pending Clarification

Chưa có câu trả lời, **không tự quyết định** khi implement:

1. **File `Order-{ordernumber}.csv` mẫu** (**chờ file mẫu**, Questions #5): header chính xác (tiếng Thụy Điển hay tiếng Anh), có BOM không, có dấu ngoặc kép không.
2. **Mã đơn `SPO…` hay `SP0…`** (**chờ file mẫu**, Questions #5): email ghi `SP0590213353056447` (số 0), còn tên folder và phiếu sản xuất ghi `SPO590213353056447` (chữ O). Cần chốt vì đây là khoá map và là path khi gọi API.
3. **Địa chỉ giao hàng không có trong CSV** (blocker): chỉ có trong PDF phiếu sản xuất. Đề xuất nhờ NWG thêm cột vào CSV (tên, địa chỉ, mã bưu chính, thành phố, nước, sđt, email, ngày đặt, mã tham chiếu của khách) hoặc gửi kèm file header riêng. NWG đã ngỏ ý "Let me know if you prefer ... additional columns". Không nên parse PDF.
4. **SKU / Leverantör SKU / product:** product trong hệ thống mình tìm theo cột nào? Lưu `SKU` (NWG) và `Leverantör SKU` vào field nào? Theo quy ước map 1:1 thì nên có 2 field riêng; có dùng lại `modelSku` (1642) hoặc `supplierSku` (1616) không? Mâu thuẫn trong dữ liệu mẫu: phiếu sản xuất ghi "Kundens artikel: `UM3410010-100-4`", còn bảng trong email ghi SKU `3410010001-100-5`.
5. **Thông tin in** (kỹ thuật DTF, mặt in, kích thước, vị trí, font/màu) chỉ có trong PDF. Xưởng chỉ đọc PDF là đủ, hay cần NWG đưa vào CSV để ghi vào `orders_products.comment`? Những file nào đính vào Korrektur: file in, preview, PDF, Namnlista?
6. **Sau khi import:** folder có được chuyển sang `archived/` không? Mình có quyền ghi/chuyển trên FTP của NWG không, hay phải tự nhớ folder nào đã xử lý (vd dựa vào order đã tồn tại)?
7. **FTP** (**chờ**, Questions #6; trước mắt test bằng `--local-dir`): "we setup FTP already" là server/tài khoản/path nào? Có dùng chung `INTEGRATION_FTP_*` (server `161.35.83.188`, tài khoản `integration`) không? Làm sao biết folder đã upload xong (vd `Order-….csv` luôn được upload cuối cùng)? Tần suất chạy cron?
8. ~~**Channel**~~ — **đã chốt: tạo channel mới nếu chưa có `synId = 'nwg'`** (Questions #4). **Còn chờ: customer** của order là NWG (1 customer cố định) hay người nhận hàng?
9. **Khi nào gửi từng trạng thái:** `InProduction` ứng với status nội bộ nào (hay gửi ngay khi import)? `Shipped` gửi lúc tạo label PostNord (order sang Complete) là đúng chưa? Đơn Complete mà không có label PostNord (giao tay, carrier khác) thì gửi gì? `Cancelled` gửi từ đâu, lý do lấy ở đâu?
10. **Đơn đã tồn tại** (cùng mã đơn, folder được upload lại hoặc chạy lại): bỏ qua, hay thay item như PINshirt/Blavitt? NWG có bao giờ sửa đơn đã gửi không?
11. **Designversion:** lưu ở đâu (field mới trên `OrderProduct`?). Khi gặp mã mới (thiết kế in đã đổi) thì chỉ ghi nhận, cảnh báo, hay chặn đơn? "The product needs to be updated on your end" nghĩa là mình phải cập nhật product nào: product theo SKU NWG hay cấu hình in (`productionPrintFileId`...)?
12. **Namnlista (tên/số theo từng áo):** lưu ở đâu? `OrderProduct.texts` chỉ nhận tối đa 3 chuỗi cho mỗi dòng, trong khi 1 dòng có thể có nhiều áo (vd 2 áo M: ANDERSSON 9, BERG 14). Có tách mỗi áo thành 1 dòng không, hay ghi gộp vào `comment`, hay chỉ đính file vào Korrektur?
13. **Giá:** CSV không có giá. Lấy theo product (product mới thì `0`) như PINshirt, hay NWG có bảng giá riêng?
14. **Ship nhiều kiện:** hệ thống mình không biết kiện nào chứa dòng nào (xem mục bẫy). Chốt luôn gửi 1 lệnh `Shipped` cho cả đơn (dùng tracking của kolli đầu) được không? Nếu cần ship từng phần thì phải có UI chọn dòng cho từng kiện, khối lượng việc lớn hơn nhiều.
15. **API NWG:** base URL + key (NWG gửi riêng). Có môi trường test/staging không? Response thành công/lỗi trông thế nào (HTTP code, body) để log và quyết định khi nào gửi lại?
16. **Hiển thị trên UI:** có cần hiển thị trạng thái đồng bộ NWG trên trang order và nút "gửi lại" không (repo React)?

---

## Thiết kế kỹ thuật (đề xuất, chốt sau khi có trả lời)

### Cấu trúc code (theo mẫu PINshirt)

| File | Vai trò |
|---|---|
| `src/Command/Cronjob/ImportOrderNwgCommand.php` | `order:import_nwg`, cùng bộ option với `order:import_pinshirt` |
| `src/Service/Nwg/NwgStorageInterface.php` + `SftpNwgStorage` / `LocalNwgStorage` | Liệt kê folder đơn, tải file, archive folder. Có thể tổng quát hoá từ `AbstractPinshirtStorage` |
| `src/Service/Nwg/NwgOrderReader.php` | Đọc/parse `Order-….csv` + `Namnlista.csv` của 1 folder thành 1 order |
| `src/Service/Nwg/NwgOrderImporter.php` | Tạo/cập nhật order (giống `PinshirtOrderImporter`) |
| `src/Service/Nwg/NwgStatusClient.php` | Gọi `POST /printer/orders/{nr}/status` (timeout, log, trả kết quả) |
| `src/Service/Nwg/NwgStatusService.php` | Dựng body cho từng trạng thái, lưu trạng thái đã gửi trên order, đánh dấu cần gửi lại |
| `src/Command/Cronjob/NwgResyncStatusCommand.php` | Gửi lại các lệnh trạng thái bị lỗi |
| `config/packages/monolog.yaml` | Channel `nwg` → `var/log/nwg-{Y-m-d}.log` |
| `config/services.yaml`, `.env` | `nwg_api_base_url`, `nwg_api_key` (key đặt trong `.env.local`) |

### Entity & Migration (đề xuất)

| Field | Bảng | Kiểu | Ý nghĩa |
|---|---|---|---|
| `nwgStatusSent` | `orders` | `varchar(32)`, nullable | Trạng thái cuối đã gửi thành công (`InProduction`/`Shipped`/`Cancelled`) |
| `nwgStatusNeedResync` | `orders` | `boolean`, default `false` | Lần gửi cuối lỗi, cron cần gửi lại |
| `nwgStatusPending` | `orders` | `varchar(32)`, nullable | Trạng thái đang chờ gửi lại |
| `nwgStatusDate` | `orders` | `timestamp`, nullable | Lần gửi cuối |
| `nwgStatusError` | `orders` | `text`, nullable | Lỗi cuối (để hiển thị/debug) |
| *(chờ #4, #11)* `designVersion`, field cho `SKU` NWG | `orders_products` | `varchar(255)`, nullable | Map 1:1 cột CSV |

Thay vì thêm nhiều cột vào `orders`, có thể làm 1 bảng log riêng `nwg_status_log` (order_id, status, request body, http code, response, created_at). Chọn cách nào sau khi chốt #9, #15, #16. Migration **viết tay** (không dùng `doctrine:migrations:diff`, xem `Version20260924090615`).

---

## TODO List

### Chuẩn bị (chờ thông tin)
- [ ] Nhận `Order-….csv` mẫu thật (#1, #2) — **chặn phần đọc CSV**
- [x] Chốt channel: tạo mới nếu chưa có `synId = 'nwg'` (#8)
- [ ] Chốt các Pending còn lại, đặc biệt #3 (địa chỉ, blocker), #4 (SKU), #8 (customer), #9 (thời điểm gửi trạng thái)
- [ ] Nhận thông tin FTP (#7, đang chờ) + base URL/key API NWG; đặt key vào `.env.local`

### Backend — Entity & Migration
- [ ] Thêm field trạng thái đồng bộ NWG (`Order` hoặc bảng log riêng) + field cho cột CSV mới trên `OrderProduct` (theo #4, #11, #12)
- [ ] Migration viết tay, có `down()`; chạy local → DEV → PROD trước khi deploy code

### Backend — Import
- [ ] `config/packages/monolog.yaml`: channel `nwg`
- [ ] `NwgStorageInterface` + `SftpNwgStorage` + `LocalNwgStorage`: liệt kê folder, tải file, archive (theo #6, #7)
- [ ] `NwgOrderReader`: đọc CSV theo tên cột, strip BOM/`\r`, kiểm tra mã đơn trùng tên folder, nối Namnlista theo `Rad`
- [ ] `NwgOrderImporter`: channel/customer/product/sub-product theo mẫu `PinshirtOrderImporter`, transaction riêng cho mỗi order, kiểm tra số item đã lưu, đính file Korrektur (#5), xử lý đơn đã tồn tại (#10)
- [ ] `ImportOrderNwgCommand` (`order:import_nwg`) + đăng ký crontab (#7)

### Backend — Gửi trạng thái
- [ ] `NwgStatusClient`: `POST {base}/printer/orders/{rawurlencode(orderNr)}/status`, header `x-functions-key`, timeout, log request/response
- [ ] `NwgStatusService`: `sendInProduction()`, `sendShipped()`, `sendCancelled()`; lỗi thì đánh dấu cần gửi lại, không throw ra luồng gọi
- [ ] `OrderService::postOrderUpdateComplete()`: thêm nhánh `createdFrom == 'import_nwg'` (trước nhánh WP) → `sendShipped()` với `postNordShipmentId` / `postNordTrackingUrl`
- [ ] Móc `sendInProduction()` vào chỗ đổi status (theo #9)
- [ ] Móc `sendCancelled()` vào luồng huỷ đơn (TSHIRTORDER-1576); chặn huỷ khi đã gửi `Shipped`
- [ ] `NwgResyncStatusCommand`: gửi lại các order có `nwgStatusNeedResync = true`

### Frontend — React *(repo khác, chờ #16)*
- [ ] Hiển thị trạng thái đồng bộ NWG trên trang order + nút gửi lại

### Test / kiểm tra
- [ ] `order:import_nwg --local-dir=... --parse-only --dump` với bộ file mẫu
- [ ] `--dry-run`, rồi import thật trên local: kiểm tra order/item/size/Korrektur
- [ ] Gửi trạng thái tới môi trường test của NWG (#15): `InProduction`, `Shipped` (sau khi tạo label PostNord), `Cancelled`; thử lỗi mạng → cron gửi lại
- [ ] Test trên DEV với folder thật trên FTP

---

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

| File | Mục đích |
|------|----------|
| `src/Command/Cronjob/ImportOrderPinshirtCommand.php` | Mẫu command import qua SFTP |
| `src/Service/Pinshirt/PinshirtOrderImporter.php` | Mẫu import 1 order (channel, customer, product, size, Korrektur) |
| `src/Service/Pinshirt/*Storage.php` | Mẫu storage SFTP/local |
| `src/Service/FileService.php` | `importOrderPinshirtProcess()`, đọc CSV PINshirt |
| `src/Service/PostNordService.php` | `orderPostnordCreateLabel()`: lưu tracking, chuyển Complete, gọi `postOrderUpdateComplete()` |
| `src/Service/OrderService.php` | `postOrderUpdateComplete()` (dòng 4770), `resyncOrderToShopify` (mẫu gửi tracking ra ngoài), `addProducts()` |
| `src/Entity/Order.php` | `postNordShipmentId`, `postNordTrackingUrl`, `postNordLatestData`, mẫu `decoNeedResync` / `decoResyncDate` |
| `src/Entity/OrderProduct.php` | `modelSku`, `modelName`, `supplierSku`, `supplierColor`, `itemId`, `comment`, `texts` |
| `src/Command/Cronjob/ResyncWpCompletedOrderCommand.php` | Mẫu cron gửi lại |
| `config/services.yaml`, `.env` | `integration_ftp_*`, `order_status_complete_id`, `order_status_web_id` |
| `docs/issues/TSHIRTORDER-1627-pinshirt-import/pinshirt-import.md` | Doc import PINshirt (các quyết định và bẫy đã gặp) |
| `docs/issues/TSHIRTORDER-1642-pinshirt-model-columns/pinshirt-model-columns.md` | `modelSku` / `modelName` |
| `docs/issues/TSHIRTORDER-1576-cancellation-function.md` | Luồng huỷ đơn |
| `files/` | Bộ file mẫu đơn `SPO590213353056447` + email gốc của NWG |
