# 11 — Script bảo trì một lần (`src/Command/Update/`)

> ⚠️ **Đọc kỹ trước khi chạy bất kỳ lệnh nào trong file này.** Đây là các Console Command **chạy tay một lần** (không nằm trong lịch cron — xem file 10 cho phần cronjob định kỳ), thường dùng để backfill/sửa dữ liệu sau một thay đổi nghiệp vụ hoặc migration. **3 lệnh được đánh dấu 🔴 có khả năng phá huỷ dữ liệu vĩnh viễn** — không chạy trên production nếu chưa có bản sao lưu (backup) và chưa hiểu rõ tác động.

## 0. Bảng phân loại theo mức độ rủi ro

| Mức | Lệnh | Vấn đề |
|---|---|---|
| 🔴 Phá huỷ, không phục hồi được | `remove:order` | **Xoá cứng** đơn hàng + toàn bộ dữ liệu con (dòng sản phẩm, file, korrektur, PDF, trạng thái sản xuất) theo `channelId` + khoảng ngày |
| 🔴 Phá huỷ, không phục hồi được | `remove_soft_delete:order` | Xoá **cứng** toàn bộ dữ liệu con của đơn (chỉ riêng bản ghi `Order` là xoá mềm) — tên gọi "soft delete" **gây hiểu lầm** vì nội dung đơn vẫn mất vĩnh viễn |
| 🔴 Phá huỷ, không phục hồi được | `update:product-order-production` | **Xoá sạch** toàn bộ `StatusList` loại `order_production_type_*` rồi tạo lại từ danh sách hard-code — mọi tuỳ biến thủ công sau lần chạy gốc sẽ bị mất |
| 🟠 Chỉ chạy 1 lần, chạy lại sẽ hỏng dữ liệu | `update:move_pdf_file_to_new_folder` | Không có cờ chống chạy lại — chạy lần 2 sẽ nhân đôi tiền tố đường dẫn file (`export_pdf/export_pdf/...`), làm hỏng mọi liên kết PDF |
| 🟡 Migration một lần, chạy lại có rủi ro thấp hơn | `update:user_multi_channel`, `update:order-price-kickback`, `update:product-procentage` | Gắn với 1 sự kiện/ngày/kênh cụ thể đã hard-code — chạy lại có thể ghi đè dữ liệu đã được chỉnh tay sau đó |
| 🟢 An toàn để chạy lại (idempotent) | Phần còn lại (xem mục 3) | Chỉ tính toán lại field phi chuẩn hoá từ dữ liệu nguồn hiện tại |

---

## 1. 🔴 Nhóm lệnh phá huỷ dữ liệu — cần đặc biệt thận trọng

### 1.1. `remove:order`

**File:** `src/Command/Update/RemoveOrderByChannelCommand.php`
**Tham số bắt buộc:** `channelId`, `createFrom` (`Y-m-d`), `createTo` (`Y-m-d`)

Xoá **cứng thật sự** (Doctrine `remove()` + `flush()`, không phải set `dateDeleted`) toàn bộ đơn hàng thuộc 1 kênh trong khoảng ngày tạo, **kèm theo mọi dữ liệu con**: `OrderProduct`, `OrderFiles`, `OrderKorrectur`, `OrderPdf`, `OrderProductionTypeStatus`, và cuối cùng là chính `Order`.

- Không có chế độ xem trước (dry-run), không có xác nhận (confirmation prompt).
- Log chỉ in ra console — **không có bất kỳ dấu vết nào được lưu lại** sau khi tiến trình kết thúc để tra soát về sau.
- **Chỉ dùng để dọn dữ liệu test/nhập nhầm** cho 1 kênh cụ thể trong khoảng ngày xác định. **Bắt buộc backup database trước khi chạy.**

### 1.2. `remove_soft_delete:order`

**File:** `src/Command/Update/RemoveOrderByChannelSoftDeleteCommand.php`
**Tham số bắt buộc:** giống hệt `remove:order`

Cùng logic khớp đơn như trên, nhưng với chính `Order` thì chỉ set `dateDeleted` (xoá mềm) thay vì xoá hẳn. **Tuy nhiên toàn bộ dữ liệu con** (`OrderProduct`, `OrderFiles`, `OrderKorrectur`, `OrderPdf`, `OrderProductionTypeStatus`) **vẫn bị xoá cứng y hệt `remove:order`**.

> ⚠️ **Đây là điểm dễ gây hiểu lầm nguy hiểm nhất trong toàn bộ codebase:** tên lệnh gợi ý "an toàn hơn vì có soft delete", nhưng thực chất **nội dung chi tiết của đơn hàng (sản phẩm, file, tiến độ sản xuất) vẫn mất vĩnh viễn** — chỉ riêng "vỏ" đơn hàng (số đơn, thông tin khách hàng snapshot) là còn có thể phục hồi được bằng cách gỡ `dateDeleted`. Cần truyền đạt rõ điều này cho bất kỳ ai định dùng lệnh này thay cho `remove:order` với kỳ vọng "an toàn hơn".

### 1.3. `update:product-order-production`

**File:** `src/Command/Update/ProductOrderProductionCommand.php`

Đây là **migration lớn tái cấu trúc toàn bộ quy trình sản xuất**, thực hiện theo thứ tự:

1. **Xoá sạch** mọi dòng `StatusList` có `type LIKE 'order_production_type_%'` (toàn bộ bước quy trình sản xuất hiện có).
2. **Tạo lại từ đầu** ~24 dòng `StatusList` hard-code sẵn tên tiếng Thuỵ Điển (VD "BRODYR", "SCREEN", "DTF", "DTG", "KLAR!"...) cho 4 nhóm loại sản xuất 203/24/25/26.
3. Reset toàn bộ bộ đếm sản xuất trên mọi đơn hàng về `null` (`Order::updateProductionCountNullForAll()`).
4. Dựng lại field sản xuất trên `OrderProduct`/`Order`/`Product` từ dữ liệu `OrderProductProduction`/`ProductProductions`.

**Đây là migration một lần gắn với 1 lần tái thiết kế quy trình sản xuất cụ thể trong lịch sử dự án.** Chạy lại lệnh này trên 1 hệ thống đã vận hành ổn định sẽ: xoá mọi bước quy trình đã được tuỳ biến thủ công sau lần chạy gốc, và reset tiến độ sản xuất đang theo dõi trên mọi đơn hàng hiện tại. **Tuyệt đối không chạy lại nếu không chắc chắn 100% về hậu quả.**

---

## 2. 🟠 Migration một lần — không được chạy 2 lần

### `update:move_pdf_file_to_new_folder`

**File:** `src/Command/Update/MovePdfFileToNewFolderCommand.php`

Duyệt **toàn bộ** bản ghi `DownPaymentFileJournalFile`, `OrderPdf`, `BulkOrderPdf`, `InvoiceFile`, cùng các `Invoice`/`InvoiceJournal` có đường dẫn PDF không rỗng, và **thêm tiền tố `export_pdf/`** vào đường dẫn file lưu trong DB — phục vụ 1 lần chuyển thư mục lưu trữ PDF.

**Không có cờ kiểm tra "đã chạy chưa"** — nếu vô tình chạy lần thứ 2, mọi đường dẫn sẽ thành `export_pdf/export_pdf/tên_file.pdf`, khiến toàn bộ liên kết tải PDF trong hệ thống bị hỏng hàng loạt (ảnh hưởng gần như mọi PDF đã từng sinh ra trước đó). **Phải xác nhận chắc chắn migration này chưa từng chạy trên môi trường đích trước khi thực thi.**

---

## 3. 🟡 Migration gắn sự kiện cụ thể (rủi ro thấp hơn nhưng không nên chạy lại tuỳ tiện)

| Lệnh | Phạm vi hard-code | Rủi ro khi chạy lại |
|---|---|---|
| `update:order-price-kickback` | Chỉ áp dụng cho đơn tạo từ ngày `2025-07-01` (hard-code trong code) | Thấp — tính lại kickback, nhưng phạm vi ngày đã lỗi thời nếu dùng cho lần sửa lỗi khác |
| `update:product-procentage` | Chỉ áp dụng kênh `synId=NWPBREW25`, `typeId=135`, ép `procentage=0.25` | Thấp — set cùng 1 giá trị cố định, nhưng là fix riêng cho 1 sự kiện, không phải công cụ tổng quát |
| `update:user_multi_channel` | Migrate user từ mô hình 1-kênh sang nhiều-kênh (`channelIds`) | **Trung bình** — nếu user đã được admin gán thêm kênh thủ công sau migration gốc, chạy lại sẽ **ghi đè về đúng 1 kênh ban đầu**, làm mất các kênh đã thêm sau này |

---

## 4. 🟢 Nhóm an toàn để chạy lại (tính toán lại từ dữ liệu nguồn — idempotent)

Các lệnh dưới đây chỉ **đọc dữ liệu nguồn hiện tại và ghi đè lại các field phi chuẩn hoá** — chạy lại nhiều lần cho kết quả nhất quán, không có tác dụng phụ tích luỹ:

| Lệnh | Việc làm |
|---|---|
| `update:add-channel-to-products` (`--fromChannelId` `--toChannelId`) | Copy liên kết kênh từ sản phẩm cha của kênh A sang kênh B (SQL `ON CONFLICT DO NOTHING` — chỉ thêm, không xoá) |
| `update:order` | Đồng bộ lại snapshot `Channel`/`Customer` trên mọi `Order`, tính lại tổng tiền + kickback |
| `update:order_production_type_count` | Tính lại bộ đếm số lượng theo từng loại sản xuất trên đơn hàng |
| `update:order-product-update-order-info` | Đồng bộ lại `orderNr`/`orderCountNr`/thuế/% kickback trên từng `OrderProduct` |
| `update:product` | Bổ sung liên kết kênh (M2M) từ field `channelId` cũ (dạng số đơn) sang bảng nối nhiều-kênh |
| `update:production-first-color` | Sửa các `OrderProduct`/`Order` đang bị đánh dấu `invalidProductionColor=yes`, tự giới hạn phạm vi (chạy xong sẽ không còn gì để sửa ở lần sau) |
| `update:product_model` | Đồng bộ lại tên/ảnh model từ `ProductModel` xuống `Product` |
| `update:invoice_due_date` | Tính lại hạn thanh toán cho mọi hoá đơn theo quy tắc hiện hành (⚠️ có thể ghi đè hạn đã chỉnh tay thủ công trên hoá đơn cũ — nên cân nhắc trước khi chạy trên dữ liệu đã có điều chỉnh đặc biệt) |
| `update:sub_product_order` | Gắn `subProductId`/`subProductSku` cho `OrderProduct` dựa theo size |
| `update:sub_product_product` | Sinh sản phẩm con (sub-product) theo size còn thiếu cho các sản phẩm cha |
| `update:sub_product_missing_name` | Điền tên còn thiếu cho sản phẩm con (lấy từ tên size) |
| `update:remove-product-without-channel` | **Xoá mềm** (không phải xoá cứng) sản phẩm + sản phẩm con không gắn kênh nào — rủi ro thấp hơn nhóm 🔴 vì có thể phục hồi bằng cách gỡ `dateDeleted`, nhưng vẫn là thao tác dọn dữ liệu hàng loạt nên cân nhắc trước khi chạy |

---

## 5. Ví dụ tình huống sử dụng đúng cách

> **Tình huống hợp lệ:** Sau khi sửa 1 lỗi tính sai % kickback áp dụng từ ngày 01/07/2025, kỹ sư chạy `update:order-price-kickback` **một lần** ngay sau khi deploy bản fix để backfill lại đúng số liệu cho các đơn đã tạo trong giai đoạn bị lỗi. Không cần chạy lại lệnh này sau đó.
>
> **Tình huống KHÔNG nên làm:** Một kỹ sư mới thấy `update:product-order-production` có tên gợi ý "cập nhật sản xuất sản phẩm cho đơn hàng" và chạy thử trên production để "làm mới lại dữ liệu sản xuất" — **đây là sai lầm nghiêm trọng**, vì lệnh này thực chất xoá sạch và tái tạo lại toàn bộ danh mục bước quy trình sản xuất, ảnh hưởng tới **mọi** đơn hàng đang hoạt động trong hệ thống, không giới hạn phạm vi nào.

## 6. Quy trình khuyến nghị trước khi chạy bất kỳ lệnh nào trong file này

1. Đọc kỹ mô tả lệnh trong file này **và** đọc trực tiếp source code lệnh đó — tên lệnh (`command name`) đôi khi **không phản ánh đúng** mức độ rủi ro thực tế (điển hình: `remove_soft_delete:order`).
2. Với mọi lệnh thuộc nhóm 🔴 và 🟠: **backup database** trước khi chạy, không có ngoại lệ.
3. Chạy thử trên môi trường staging/dev với dữ liệu tương tự trước khi chạy trên production, nếu có thể.
4. Sau khi chạy, **tự ghi lại thủ công** (VD trong changelog nội bộ) đã chạy lệnh gì, tham số gì, thời điểm nào — vì bản thân các lệnh này **không tự ghi log bền vững** ngoài console output.
